import { feature } from 'bun:bundle' import { z } from 'zod/v4' import { getSessionId } from '../../bootstrap/state.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' import { buildTool, type ToolDef } from '../../Tool.js' import { lazySchema } from '../../utils/lazySchema.js' import { isTodoV2Enabled } from '../../utils/tasks.js' import { TodoListSchema } from '../../utils/todo/types.js' import { VERIFICATION_AGENT_TYPE } from '../AgentTool/constants.js' import { TODO_WRITE_TOOL_NAME } from './constants.js' import { DESCRIPTION, PROMPT } from './prompt.js' const inputSchema = lazySchema(() => z.strictObject({ todos: TodoListSchema().describe('The updated todo list'), }), ) type InputSchema = ReturnType const outputSchema = lazySchema(() => z.object({ oldTodos: TodoListSchema().describe('The todo list before the update'), newTodos: TodoListSchema().describe('The todo list after the update'), verificationNudgeNeeded: z.boolean().optional(), }), ) type OutputSchema = ReturnType export type Output = z.infer export const TodoWriteTool = buildTool({ name: TODO_WRITE_TOOL_NAME, searchHint: 'manage the session task checklist', maxResultSizeChars: 100_000, strict: true, async description() { return DESCRIPTION }, async prompt() { return PROMPT }, get inputSchema(): InputSchema { return inputSchema() }, get outputSchema(): OutputSchema { return outputSchema() }, userFacingName() { return '' }, shouldDefer: true, isEnabled() { return !isTodoV2Enabled() }, toAutoClassifierInput(input) { return `${input.todos.length} items` }, async checkPermissions(input) { // No permission checks required for todo operations return { behavior: 'allow', updatedInput: input } }, renderToolUseMessage() { return null }, async call({ todos }, context) { const appState = context.getAppState() const todoKey = context.agentId ?? getSessionId() const oldTodos = appState.todos[todoKey] ?? [] const allDone = todos.every(_ => _.status === 'completed') const newTodos = allDone ? [] : todos // Structural nudge: if the main-thread agent is closing out a 3+ item // list and none of those items was a verification step, append a reminder // to the tool result. Fires at the exact loop-exit moment where skips // happen ("when the last task closed, the loop exited"). let verificationNudgeNeeded = false if ( feature('VERIFICATION_AGENT') && getFeatureValue_CACHED_MAY_BE_STALE('tengu_hive_evidence', false) && !context.agentId && allDone && todos.length >= 3 && !todos.some(t => /verif/i.test(t.content)) ) { verificationNudgeNeeded = true } context.setAppState(prev => ({ ...prev, todos: { ...prev.todos, [todoKey]: newTodos, }, })) return { data: { oldTodos, newTodos: todos, verificationNudgeNeeded, }, } }, mapToolResultToToolResultBlockParam({ verificationNudgeNeeded }, toolUseID) { const base = `Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable` const nudge = verificationNudgeNeeded ? `\n\nNOTE: You just closed out 3+ tasks and none of them was a verification step. Before writing your final summary, spawn the verification agent (subagent_type="${VERIFICATION_AGENT_TYPE}"). You cannot self-assign PARTIAL by listing caveats in your summary \u2014 only the verifier issues a verdict.` : '' return { tool_use_id: toolUseID, type: 'tool_result', content: base + nudge, } }, } satisfies ToolDef)