codev / src /tools /GoalUpdateTool /GoalUpdateTool.ts
chenbhao's picture
fix: goal can resuem by /resume
2c4e38d
Raw
History Blame Contribute Delete
3.91 kB
import { z } from 'zod'
import { buildTool, type ToolDef } from '../../Tool.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import { saveGoal } from '../../utils/sessionStorage.js'
import {
DESCRIPTION,
UPDATE_GOAL_TOOL_NAME,
UPDATE_GOAL_TOOL_PROMPT,
} from './prompt.js'
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
const inputSchema = lazySchema(() =>
z.strictObject({
goal_id: z
.string()
.min(1)
.describe(
'The active goal id from the latest goal continuation prompt.',
),
status: z
.enum(['complete', 'blocked'])
.describe(
"'complete' if the goal is fully achieved with no required work remaining; 'blocked' only when the same blocking condition has repeated for 3+ consecutive goal turns.",
),
reason: z
.string()
.min(1)
.max(500)
.describe('One-sentence explanation visible to the user.'),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
const outputSchema = lazySchema(() =>
z.object({
status: z.enum(['complete', 'blocked', 'no-active-goal', 'stale-goal']),
reason: z.string(),
message: z.string(),
}),
)
type OutputSchema = ReturnType<typeof outputSchema>
export type Output = z.infer<OutputSchema>
export const GoalUpdateTool = buildTool({
name: UPDATE_GOAL_TOOL_NAME,
searchHint: 'declare goal outcome — complete or blocked',
maxResultSizeChars: 4_000,
userFacingName: () => 'Update Goal',
get inputSchema(): InputSchema {
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema()
},
isReadOnly() {
return false
},
isConcurrencySafe() {
return true
},
toAutoClassifierInput(input) {
return input.reason
},
async description() {
return DESCRIPTION
},
async prompt() {
return UPDATE_GOAL_TOOL_PROMPT
},
mapToolResultToToolResultBlockParam(output, toolUseID) {
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: jsonStringify(output),
}
},
renderToolUseMessage,
renderToolResultMessage,
async call({ goal_id, status, reason }, { getAppState, setAppState }) {
const goal = getAppState().goal
if (
!goal ||
(goal.status !== 'pursuing' && goal.status !== 'paused')
) {
return {
data: {
status: 'no-active-goal' as const,
reason,
message: 'No active goal — nothing to update.',
},
}
}
if (goal.id !== goal_id) {
return {
data: {
status: 'stale-goal' as const,
reason,
message:
'Goal id does not match the active goal — ignoring stale update.',
},
}
}
const now = Date.now()
const internalStatus =
status === 'complete'
? ('achieved' as const)
: ('blocked' as const)
let updatedGoal: typeof goal | null = null
setAppState(prev => {
const current = prev.goal
if (
!current ||
current.id !== goal_id ||
(current.status !== 'pursuing' && current.status !== 'paused')
) {
return prev
}
updatedGoal = {
...current,
status: internalStatus,
lastReason: reason,
lastUpdatedAt: now,
}
return {
...prev,
goal: updatedGoal,
}
})
if (!updatedGoal) {
return {
data: {
status: 'stale-goal' as const,
reason,
message:
'Goal changed before the update was applied — ignoring stale update.',
},
}
}
saveGoal({
type: 'goal',
...updatedGoal,
})
return {
data: {
status,
reason,
message: `Goal marked ${status}.`,
},
}
},
} satisfies ToolDef<InputSchema, Output>)