import { feature } from 'bun:bundle'import { z } from 'zod/v4'import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'import { buildTool, type ToolDef } from '../../Tool.js'import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'import {executeTaskCompletedHooks,getTaskCompletedHookMessage,} from '../../utils/hooks.js'import { lazySchema } from '../../utils/lazySchema.js'import {blockTask,deleteTask,getTask,getTaskListId,isTodoV2Enabled,listTasks,type TaskStatus,TaskStatusSchema,updateTask,} from '../../utils/tasks.js'import {getAgentId,getAgentName,getTeammateColor,getTeamName,} from '../../utils/teammate.js'import { writeToMailbox } from '../../utils/teammateMailbox.js'import { VERIFICATION_AGENT_TYPE } from '../AgentTool/constants.js'import { TASK_UPDATE_TOOL_NAME } from './constants.js'import { DESCRIPTION, PROMPT } from './prompt.js'const inputSchema = lazySchema(() => {// Extended status schema that includes 'deleted' as a special actionconst TaskUpdateStatusSchema = TaskStatusSchema().or(z.literal('deleted'))return z.strictObject({taskId: z.string().describe('The ID of the task to update'),subject: z.string().optional().describe('New subject for the task'),description: z.string().optional().describe('New description for the task'),activeForm: z.string().optional().describe('Present continuous form shown in spinner when in_progress (e.g., "Running tests")',),status: TaskUpdateStatusSchema.optional().describe('New status for the task',),addBlocks: z.array(z.string()).optional().describe('Task IDs that this task blocks'),addBlockedBy: z.array(z.string()).optional().describe('Task IDs that block this task'),owner: z.string().optional().describe('New owner for the task'),metadata: z.record(z.string(), z.unknown()).optional().describe('Metadata keys to merge into the task. Set a key to null to delete it.',),})})type InputSchema = ReturnType<typeof inputSchema>const outputSchema = lazySchema(() =>z.object({success: z.boolean(),taskId: z.string(),updatedFields: z.array(z.string()),error: z.string().optional(),statusChange: z.object({from: z.string(),to: z.string(),}).optional(),verificationNudgeNeeded: z.boolean().optional(),}),)type OutputSchema = ReturnType<typeof outputSchema>export type Output = z.infer<OutputSchema>export const TaskUpdateTool = buildTool({name: TASK_UPDATE_TOOL_NAME,searchHint: 'update a task',maxResultSizeChars: 100_000,async description() {return DESCRIPTION},async prompt() {return PROMPT},get inputSchema(): InputSchema {return inputSchema()},get outputSchema(): OutputSchema {return outputSchema()},userFacingName() {return 'TaskUpdate'},shouldDefer: true,isEnabled() {return isTodoV2Enabled()},isConcurrencySafe() {return true},toAutoClassifierInput(input) {const parts = [input.taskId]if (input.status) parts.push(input.status)if (input.subject) parts.push(input.subject)return parts.join('')},renderToolUseMessage() {return null},async call({taskId,subject,description,activeForm,status,owner,addBlocks,addBlockedBy,metadata,},context,) {const taskListId = getTaskListId()// Auto-expand task list when updating taskscontext.setAppState(prev => {if (prev.expandedView === 'tasks') return prevreturn { ...prev, expandedView: 'tasks' as const }})// Check if task existsconst existingTask = await getTask(taskListId, taskId)if (!existingTask) {return {data: {success: false,taskId,updatedFields: [],error: 'Task not found',},}}const updatedFields: string[] = []// Update basic fields if provided and different from current valueconst updates: {subject?: stringdescription?: stringactiveForm?: stringstatus?: TaskStatusowner?: stringmetadata?: Record<string, unknown>} = {}if (subject !== undefined && subject !== existingTask.subject) {updates.subject = subjectupdatedFields.push('subject')}if (description !== undefined && description !== existingTask.description) {updates.description = descriptionupdatedFields.push('description')}if (activeForm !== undefined && activeForm !== existingTask.activeForm) {updates.activeForm = activeFormupdatedFields.push('activeForm')}if (owner !== undefined && owner !== existingTask.owner) {updates.owner = ownerupdatedFields.push('owner')}// Auto-set owner when a teammate marks a task as in_progress without// explicitly providing an owner. This ensures the task list can match// todo items to teammates for showing activity status.if (isAgentSwarmsEnabled() &&status === 'in_progress' &&owner === undefined &&!existingTask.owner) {const agentName = getAgentName()if (agentName) {updates.owner = agentNameupdatedFields.push('owner')}}if (metadata !== undefined) {const merged = { ...(existingTask.metadata ?? {}) }for (const [key, value] of Object.entries(metadata)) {if (value === null) {delete merged[key]} else {merged[key] = value}}updates.metadata = mergedupdatedFields.push('metadata')}if (status !== undefined) {// Handle deletion - delete the task file and return earlyif (status === 'deleted') {const deleted = await deleteTask(taskListId, taskId)return {data: {success: deleted,taskId,updatedFields: deleted ? ['deleted'] : [],error: deleted ? undefined : 'Failed to delete task',statusChange: deleted? { from: existingTask.status, to: 'deleted' }: undefined,},}}// For regular status updates, validate and apply if differentif (status !== existingTask.status) {// Run TaskCompleted hooks when marking a task as completedif (status === 'completed') {const blockingErrors: string[] = []const generator = executeTaskCompletedHooks(taskId,existingTask.subject,existingTask.description,getAgentName(),getTeamName(),undefined,context?.abortController?.signal,undefined,context,)for await (const result of generator) {if (result.blockingError) {blockingErrors.push(getTaskCompletedHookMessage(result.blockingError),)}}if (blockingErrors.length > 0) {return {data: {success: false,taskId,updatedFields: [],error: blockingErrors.join('\n'),},}}}updates.status = statusupdatedFields.push('status')}}if (Object.keys(updates).length > 0) {await updateTask(taskListId, taskId, updates)}// Notify new owner via mailbox when ownership changesif (updates.owner && isAgentSwarmsEnabled()) {const senderName = getAgentName() || 'team-lead'const senderColor = getTeammateColor()const assignmentMessage = JSON.stringify({type: 'task_assignment',taskId,subject: existingTask.subject,description: existingTask.description,assignedBy: senderName,timestamp: new Date().toISOString(),})await writeToMailbox(updates.owner,{from: senderName,text: assignmentMessage,timestamp: new Date().toISOString(),color: senderColor,},taskListId,)}// Add blocks if provided and not already presentif (addBlocks && addBlocks.length > 0) {const newBlocks = addBlocks.filter(id => !existingTask.blocks.includes(id),)for (const blockId of newBlocks) {await blockTask(taskListId, taskId, blockId)}if (newBlocks.length > 0) {updatedFields.push('blocks')}}// Add blockedBy if provided and not already present (reverse: the blocker blocks this task)if (addBlockedBy && addBlockedBy.length > 0) {const newBlockedBy = addBlockedBy.filter(id => !existingTask.blockedBy.includes(id),)for (const blockerId of newBlockedBy) {await blockTask(taskListId, blockerId, taskId)}if (newBlockedBy.length > 0) {updatedFields.push('blockedBy')}}// Structural verification nudge: if the main-thread agent just closed// out a 3+ task list and none of those tasks was a verification step,// append a reminder to the tool result. Fires at the loop-exit moment// where skips happen ("when the last task closed, the loop exited").// Mirrors the TodoWriteTool nudge for V1 sessions; this covers V2// (interactive CLI). TaskUpdateToolOutput is @internal so this field// does not touch the public SDK surface.let verificationNudgeNeeded = falseif (feature('VERIFICATION_AGENT') &&getFeatureValue_CACHED_MAY_BE_STALE('tengu_hive_evidence', false) &&!context.agentId &&updates.status === 'completed') {const allTasks = await listTasks(taskListId)const allDone = allTasks.every(t => t.status === 'completed')if (allDone &&allTasks.length >= 3 &&!allTasks.some(t => /verif/i.test(t.subject))) {verificationNudgeNeeded = true}}return {data: {success: true,taskId,updatedFields,statusChange:updates.status !== undefined? { from: existingTask.status, to: updates.status }: undefined,verificationNudgeNeeded,},}},mapToolResultToToolResultBlockParam(content, toolUseID) {const {success,taskId,updatedFields,error,statusChange,verificationNudgeNeeded,} = content as Outputif (!success) {// Return as non-error so it doesn't trigger sibling tool cancellation// in StreamingToolExecutor. "Task not found" is a benign condition// (e.g., task list already cleaned up) that the model can handle.return {tool_use_id: toolUseID,type: 'tool_result',content: error || `Task #${taskId} not found`,}}let resultContent = `Updated task #${taskId}${updatedFields.join(', ')}`// Add reminder for teammates when they complete a task (supports in-process teammates)if (statusChange?.to === 'completed' &&getAgentId() &&isAgentSwarmsEnabled()) {resultContent +='\n\nTask completed. Call TaskList now to find your next available task or see if your work unblocked others.'}if (verificationNudgeNeeded) {resultContent += `\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 — only the verifier issues a verdict.`}return {tool_use_id: toolUseID,type: 'tool_result',content: resultContent,}},} satisfies ToolDef<InputSchema, Output>)
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。