/** * Model-facing `get_goal`, `create_goal`, and `update_goal` tools over the * persisted same-session goal domain. * @module @deepseek-ai/dsh-tool-goal */ import type { Context } from 'cordis' import z from 'schemastery' import { GoalId } from '@deepseek-ai/dsh-goal' import type { GoalRef, GoalView } from '@deepseek-ai/dsh-goal' import { HarnessError } from '@deepseek-ai/dsh-llm' import { defineTool } from '@deepseek-ai/dsh-tools' import type { GenericCallView } from '@deepseek-ai/dsh-tools' import type {} from '@deepseek-ai/dsh-system-prompt' import { completionAuthority, goalToolExecution, requireDirectHuman, } from './authority.ts' export const name = 'tool-goal' export const inject = ['agents', 'goals', 'tools', 'systemPrompt'] /** Model policy and hard lower bounds for goal-state updates. */ export interface Config { /** Minimum admitted goal rounds before the model may self-report `blocked`. */ blockedAfterConsecutiveRounds?: number } /** Schemastery config for the goal-tool policy. */ export const Config: z = z.object({ blockedAfterConsecutiveRounds: z.number().step(1).min(1).default(3), }) /** Fully materialized tool policy. */ interface ResolvedConfig { readonly blockedAfterConsecutiveRounds: number } type UpdateAction = 'edit' | 'pause' | 'resume' | 'complete' | 'blocked' const UPDATE_ACTIONS: UpdateAction[] = ['edit', 'pause', 'resume', 'complete', 'blocked'] const CREATE_DESCRIPTION = 'Create one persisted same-session completion goal when the current direct human request ' + 'is a long-running objective that should continue across autonomous goal rounds. You may ' + 'infer that intent without requiring the user to say "create a goal". Do not use this for ' + 'trivial single-turn work. Execution rejects non-human and subagent authority.' const GET_DESCRIPTION = 'Read the current same-session goal, including its exact id/revision, objective, phase, completed ' + 'continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. ' + 'Call this before updating a goal.' /** Canonical goal-tool output, matching the existing compact Native JSON. */ type GoalToolValue = | { goal: null } | { goal: { id: string revision: number objective: string phase: GoalView['phase'] roundsStarted: number maxGoalRounds: number blockedReason?: { code: string; message: string } } activation: GoalView['activation'] } const GOAL_VALUE_SCHEMA = { oneOf: [ { type: 'object', additionalProperties: false, properties: { goal: { type: 'null', required: true }, }, }, { type: 'object', additionalProperties: false, properties: { goal: { type: 'object', additionalProperties: false, required: true, properties: { id: { type: 'string', required: true }, revision: { type: 'integer', required: true }, objective: { type: 'string', required: true }, phase: { type: 'string', required: true, enum: ['active', 'paused', 'blocked', 'complete'] }, roundsStarted: { type: 'integer', required: true }, maxGoalRounds: { type: 'integer', required: true }, blockedReason: { type: 'object', additionalProperties: false, properties: { code: { type: 'string', required: true }, message: { type: 'string', required: true }, }, }, }, }, activation: { type: 'string', required: true, enum: ['armed', 'disarmed'] }, }, }, ], } as const /** Render policy guidance with its deployment-selected blocked threshold. */ function guidance(blockedAfter: number): string { return 'Use goal tools for one long-running completion objective in the current session. ' + 'create_goal may infer goal intent from a direct human request in any language; do not ' + 'create a goal for routine single-turn work. Call get_goal before update_goal and copy its ' + 'exact goal_id and revision. After session resume or fork, an active goal is disarmed: when ' + 'a human asks to continue or resume in any wording or language, use update_goal action ' + 'resume to rearm it. Mark complete only when the objective is actually achieved. Mark ' + `blocked only after the same blocking condition persists for at least ${blockedAfter} ` + 'consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, ' + 'or useful remaining work is not blocked.' } /** Validate config even when apply is called directly outside Loader normalization. */ function resolveConfig(config: Config): ResolvedConfig { const blockedAfter = config.blockedAfterConsecutiveRounds ?? 3 if (!Number.isSafeInteger(blockedAfter) || blockedAfter < 1) { throw new TypeError('blockedAfterConsecutiveRounds must be a positive safe integer') } return { blockedAfterConsecutiveRounds: blockedAfter } } /** Whether optional text is meaningful rather than a strict-schema empty filler. */ function hasText(value: string | undefined): value is string { return value !== undefined && value !== '' } /** Whether an optional round cap is meaningful rather than a strict-schema zero filler. */ function hasRoundCap(value: number | undefined): value is number { return value !== undefined && value !== 0 } /** Build the exact compare-and-set ref from model arguments. */ function goalRef(goalId: string, revision: number): GoalRef { if (goalId.length === 0 || goalId !== goalId.trim() || !Number.isSafeInteger(revision) || revision < 1) { throw new HarnessError( 'goal_id must be non-empty and revision must be a positive safe integer', 'GOAL_TOOL_INVALID_UPDATE', ) } return { id: GoalId(goalId), revision } } /** Stable compact model result; activation is an observation, not replay state. */ function goalValue(goal: GoalView | undefined): GoalToolValue { if (goal === undefined) return { goal: null } return { goal: { id: goal.id, revision: goal.revision, objective: goal.objective, phase: goal.phase, roundsStarted: goal.roundsStarted, maxGoalRounds: goal.maxGoalRounds, ...goal.blockedReason === undefined ? {} : { blockedReason: { code: goal.blockedReason.code, message: goal.blockedReason.message }, }, }, activation: goal.activation, } } /** Reusable canonical output declaration for all three goal controls. */ const GOAL_OUTPUT = { schema: GOAL_VALUE_SCHEMA, render: (_args: unknown, value: GoalToolValue) => [{ type: 'text' as const, text: JSON.stringify(value) }], } /** Generic, args-only pending presentation shared by the goal tools. */ function present(title: string, kind: 'read' | 'other', rawInput?: unknown): GenericCallView { return { card: 'generic', title, kind, ...rawInput === undefined ? {} : { rawInput } } } /** Register the three Codex-shaped goal tools and their shared policy section. */ export function apply(ctx: Context, config: Config): void { const resolved = resolveConfig(config) ctx.systemPrompt.section({ name: 'tool:goal', order: 114, text: guidance(resolved.blockedAfterConsecutiveRounds), }) ctx.tools.register(defineTool({ name: 'get_goal', description: GET_DESCRIPTION, parameters: {}, output: GOAL_OUTPUT, execute(_args, exec) { const execution = goalToolExecution(ctx, exec) return Promise.resolve(goalValue(ctx.goals.get(execution.agent))) }, presentCall: () => present('Read current goal', 'read'), })) ctx.tools.register(defineTool({ name: 'create_goal', description: CREATE_DESCRIPTION, parameters: { objective: { type: 'string', required: true, description: 'The concrete completion objective inferred from the direct human request.', }, max_goal_rounds: { type: 'number', description: 'Optional positive safe-integer limit on automatic continuation rounds.', }, }, output: GOAL_OUTPUT, execute(args, exec) { const execution = goalToolExecution(ctx, exec) requireDirectHuman(ctx, execution) const goal = ctx.goals.create(execution.agent, { objective: args.objective, ...args.max_goal_rounds === undefined ? {} : { maxGoalRounds: args.max_goal_rounds }, }) return Promise.resolve(goalValue(goal)) }, presentCall: args => present('Create goal', 'other', args.objective), })) ctx.tools.register(defineTool({ name: 'update_goal', description: 'Update the exact current goal revision. edit, pause, and resume require a direct ' + 'top-level human request. During an automatic continuation of the current goal, complete ' + 'and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains ' + 'responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason.', parameters: { goal_id: { type: 'string', required: true, description: 'Exact id returned by get_goal.' }, revision: { type: 'number', required: true, description: 'Exact positive revision returned by get_goal.' }, action: { type: 'string', required: true, enum: UPDATE_ACTIONS, description: 'edit | pause | resume | complete | blocked', }, objective: { type: 'string', description: 'Replacement objective; valid only with action edit.' }, max_goal_rounds: { type: 'number', description: 'Replacement cap; valid only with action edit.' }, blocked_reason: { type: 'string', description: 'Concrete blocking condition; required only with action blocked.', }, }, output: GOAL_OUTPUT, execute(args, exec) { const execution = goalToolExecution(ctx, exec) const ref = goalRef(args.goal_id, args.revision) const replacements = { ...hasText(args.objective) ? { objective: args.objective } : {}, ...hasRoundCap(args.max_goal_rounds) ? { maxGoalRounds: args.max_goal_rounds } : {}, } if (args.action === 'edit') { requireDirectHuman(ctx, execution) if (hasText(args.blocked_reason)) { throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE') } const goal = ctx.goals.edit(execution.agent, ref, replacements) return Promise.resolve(goalValue(goal)) } if (args.action === 'pause' || args.action === 'resume') { requireDirectHuman(ctx, execution) if (hasText(args.objective) || hasRoundCap(args.max_goal_rounds) || hasText(args.blocked_reason)) { throw new HarnessError( 'objective and max_goal_rounds are valid only with action edit; blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE', ) } const goal = args.action === 'pause' ? ctx.goals.pause(execution.agent, ref) : ctx.goals.resume(execution.agent, ref) return Promise.resolve(goalValue(goal)) } const authority = completionAuthority(ctx, execution) if (hasText(args.objective) || hasRoundCap(args.max_goal_rounds)) { throw new HarnessError( 'objective and max_goal_rounds are valid only with action edit', 'GOAL_TOOL_INVALID_UPDATE', ) } if (args.action === 'complete' && hasText(args.blocked_reason)) { throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE') } if (args.action === 'blocked' && (args.blocked_reason === undefined || args.blocked_reason.trim().length === 0)) { throw new HarnessError('blocked_reason is required with action blocked', 'GOAL_TOOL_INVALID_UPDATE') } if (args.action === 'blocked' && authority.kind === 'goal-round' && authority.goal.roundsStarted < resolved.blockedAfterConsecutiveRounds) { throw new HarnessError( `blocked requires at least ${resolved.blockedAfterConsecutiveRounds} consecutive goal rounds; ` + `current round is ${authority.goal.roundsStarted}`, 'GOAL_TOOL_BLOCK_THRESHOLD', ) } const goal = args.action === 'complete' ? ctx.goals.complete(execution.agent, ref) : ctx.goals.block(execution.agent, ref, { code: 'model-reported', message: args.blocked_reason as string, }) if (authority.kind === 'goal-round') exec.concludeTurn() return Promise.resolve(goalValue(goal)) }, presentCall: args => present( `${args.action === 'blocked' ? 'Mark' : args.action.charAt(0).toUpperCase() + args.action.slice(1)} goal`, 'other', hasText(args.blocked_reason) ? args.blocked_reason : hasText(args.objective) ? args.objective : hasRoundCap(args.max_goal_rounds) ? args.max_goal_rounds : args.goal_id, ), })) }