Four values complete the vocabulary, so the opaque body is reached only by producers that genuinely promise no shape. `snapshot` — current state a later snapshot supersedes. system-prompt now exposes `renderContextSections()`, the named contributions `renderContextSnapshot()` already joins for the model, so the body attributes each part to the subsystem that produced it instead of re-splitting joined prose. The runtime snapshot, time-context, and tmux-context declare it. `notice` — a one-off account of what just happened, declared by tool-tasks, goal state changes, tool-goal wrap-up, plan-mode switches, and repeat-tool-guard. Its `summary` rides the COLLAPSED row: these five are the majority of shipped producers and none of them needs expanding to be read. The task summary bounds itself because its inputs are unbounded caller text. `relay` — a message another agent addressed to this one; both subagent sources declare it and the body names the sender above what it said. `recall` — material lifted from another session's log. session-reference needed no new field: its references already record retained and omitted counts and the truncation flag, which the body shows first, because recalled context is bounded on the way in. `ContextFormed` is now discriminated by `form`, so a producer cannot declare a shape without the facts that shape is presented from — a notice without its summary, or a snapshot without its sections, fails to compile. Only the two hook bridges stay opaque, by design: their content is whatever an external program printed, so no shape can be promised for it. Unknown kinds and unreadable records land there too.
339 lines
13 KiB
TypeScript
339 lines
13 KiB
TypeScript
/**
|
|
* 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 { createUserMessage, 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'
|
|
import { renderWrapupContext } from './wrapup.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<Config> = 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.deferContext(createUserMessage({
|
|
content: args.action === 'complete'
|
|
? renderWrapupContext(goal.objective)
|
|
: renderWrapupContext(goal.objective, args.blocked_reason as string),
|
|
source: {
|
|
kind: 'plugin',
|
|
plugin: 'tool-goal',
|
|
form: 'notice',
|
|
summary: `${args.action as string}: ${goal.objective}`,
|
|
},
|
|
}))
|
|
}
|
|
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,
|
|
),
|
|
}))
|
|
}
|