File size: 3,908 Bytes
fdc8b59 2c4e38d fdc8b59 2c4e38d fdc8b59 2c4e38d fdc8b59 2c4e38d fdc8b59 2c4e38d fdc8b59 2c4e38d fdc8b59 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | 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>) |