mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
277 lines
12 KiB
TypeScript
277 lines
12 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 type { Agent } from '@deepseek-ai/dsh-agent'
|
|
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'
|
|
import type { GoalToolExecution } 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<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.'
|
|
|
|
/** 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 }
|
|
}
|
|
|
|
/** 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 renderGoal(goal: GoalView | undefined): string {
|
|
if (goal === undefined) return JSON.stringify({ goal: null })
|
|
return JSON.stringify({
|
|
goal: {
|
|
id: goal.id,
|
|
revision: goal.revision,
|
|
objective: goal.objective,
|
|
phase: goal.phase,
|
|
roundsStarted: goal.roundsStarted,
|
|
maxGoalRounds: goal.maxGoalRounds,
|
|
...goal.blockedReason === undefined ? {} : { blockedReason: goal.blockedReason },
|
|
},
|
|
activation: goal.activation,
|
|
})
|
|
}
|
|
|
|
/** 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 } }
|
|
}
|
|
|
|
/** Remember whether one autonomous terminal report should stop this turn. */
|
|
function observeMutation(
|
|
terminalTurns: WeakMap<Agent, number>,
|
|
execution: GoalToolExecution,
|
|
autonomousTerminal: boolean,
|
|
): void {
|
|
if (!autonomousTerminal) {
|
|
terminalTurns.delete(execution.agent)
|
|
return
|
|
}
|
|
terminalTurns.set(execution.agent, execution.start.data.turn)
|
|
}
|
|
|
|
/** Register the three Codex-shaped goal tools and their shared policy section. */
|
|
export function apply(ctx: Context, config: Config): void {
|
|
const resolved = resolveConfig(config)
|
|
// A stale entry cannot match a later loop turn because turn numbers increase
|
|
// monotonically within the agent's fixed session.
|
|
const terminalTurns = new WeakMap<Agent, number>()
|
|
ctx.on('agent/turn-stop', (agent, turn) => {
|
|
if (terminalTurns.get(agent) !== turn) return undefined
|
|
terminalTurns.delete(agent)
|
|
return { action: 'stop' }
|
|
})
|
|
ctx.systemPrompt.section({
|
|
name: 'tool:goal',
|
|
order: 114,
|
|
text: guidance(resolved.blockedAfterConsecutiveRounds),
|
|
})
|
|
|
|
ctx.tools.register(defineTool({
|
|
name: 'get_goal',
|
|
description: GET_DESCRIPTION,
|
|
parameters: {},
|
|
execute(_args, exec) {
|
|
const execution = goalToolExecution(ctx, exec)
|
|
return Promise.resolve([{
|
|
type: 'text',
|
|
text: renderGoal(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.',
|
|
},
|
|
},
|
|
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 },
|
|
})
|
|
observeMutation(terminalTurns, execution, false)
|
|
return Promise.resolve([{ type: 'text', text: renderGoal(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.',
|
|
},
|
|
},
|
|
execute(args, exec) {
|
|
const execution = goalToolExecution(ctx, exec)
|
|
const ref = goalRef(args.goal_id, args.revision)
|
|
const replacements = {
|
|
...args.objective === undefined ? {} : { objective: args.objective },
|
|
...args.max_goal_rounds === undefined ? {} : { maxGoalRounds: args.max_goal_rounds },
|
|
}
|
|
if (args.action === 'edit') {
|
|
requireDirectHuman(ctx, execution)
|
|
if (args.blocked_reason !== undefined) {
|
|
throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE')
|
|
}
|
|
const goal = ctx.goals.edit(execution.agent, ref, replacements)
|
|
observeMutation(terminalTurns, execution, false)
|
|
return Promise.resolve([{
|
|
type: 'text',
|
|
text: renderGoal(goal),
|
|
}])
|
|
}
|
|
if (args.action === 'pause' || args.action === 'resume') {
|
|
requireDirectHuman(ctx, execution)
|
|
if (args.objective !== undefined || args.max_goal_rounds !== undefined || args.blocked_reason !== undefined) {
|
|
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)
|
|
observeMutation(terminalTurns, execution, false)
|
|
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
|
}
|
|
const authority = completionAuthority(ctx, execution)
|
|
if (args.objective !== undefined || args.max_goal_rounds !== undefined) {
|
|
throw new HarnessError(
|
|
'objective and max_goal_rounds are valid only with action edit',
|
|
'GOAL_TOOL_INVALID_UPDATE',
|
|
)
|
|
}
|
|
if (args.action === 'complete' && args.blocked_reason !== undefined) {
|
|
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,
|
|
})
|
|
observeMutation(terminalTurns, execution, authority.kind === 'goal-round')
|
|
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
|
},
|
|
presentCall: args => present(
|
|
`${args.action === 'blocked' ? 'Mark' : args.action.charAt(0).toUpperCase() + args.action.slice(1)} goal`,
|
|
'other',
|
|
args.blocked_reason ?? args.objective ?? args.goal_id,
|
|
),
|
|
}))
|
|
}
|