feat(goal): add model-facing goal tools
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/** Execution-time authority checks for the model-facing goal tools. */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { GoalView } from '@deepseek-ai/dsh-goal'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
type TurnStartEvent = Extract<SessionEvent, { type: 'turn/start' }>
|
||||
|
||||
/** Current open turn plus the events accepted after its start boundary. */
|
||||
export interface GoalToolExecution {
|
||||
readonly agent: Agent
|
||||
readonly start: TurnStartEvent
|
||||
readonly events: readonly SessionEvent[]
|
||||
}
|
||||
|
||||
/** Hard authority granted to one state-changing call. */
|
||||
export type GoalToolAuthority =
|
||||
| { readonly kind: 'direct-human' }
|
||||
| { readonly kind: 'goal-round'; readonly goal: GoalView }
|
||||
|
||||
/** Throw one structured tool-policy failure. */
|
||||
function reject(message: string, code = 'GOAL_TOOL_AUTHORITY_REQUIRED'): never {
|
||||
throw new HarnessError(message, code)
|
||||
}
|
||||
|
||||
/** Locate the open turn enclosing a model tool call. */
|
||||
function openTurn(agent: Agent): { start: TurnStartEvent; events: readonly SessionEvent[] } {
|
||||
const events = agent.session.events
|
||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||
const boundary = events[index]
|
||||
if (boundary?.type === 'turn/end') {
|
||||
reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED')
|
||||
}
|
||||
if (boundary?.type === 'turn/start') {
|
||||
return { start: boundary, events: events.slice(index + 1) }
|
||||
}
|
||||
}
|
||||
return reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and authenticate the calling agent and its driver boundary.
|
||||
* @param ctx - Context carrying the live agent registry.
|
||||
* @param exec - Tool execution metadata supplied by the registry.
|
||||
* @returns The authenticated agent and its current turn window.
|
||||
*/
|
||||
export function goalToolExecution(ctx: Context, exec: ToolRunContext): GoalToolExecution {
|
||||
const agent = exec.agent
|
||||
if (agent === undefined) {
|
||||
return reject('goal tools require a calling agent', 'GOAL_TOOL_AGENT_REQUIRED')
|
||||
}
|
||||
if (ctx.agents.get(agent.id) !== agent || agent.status !== 'running'
|
||||
|| ctx.agents.currentInitiator() !== agent) {
|
||||
return reject(
|
||||
'goal tools require the exact live calling agent inside its active driver',
|
||||
'GOAL_TOOL_DRIVER_REQUIRED',
|
||||
)
|
||||
}
|
||||
return { agent, ...openTurn(agent) }
|
||||
}
|
||||
|
||||
/** Whether an accepted human message appears in the current root-agent turn. */
|
||||
function hasDirectHumanInput(ctx: Context, execution: GoalToolExecution): boolean {
|
||||
if (!ctx.agents.roots().includes(execution.agent)) return false
|
||||
return execution.events.some(event =>
|
||||
(event.type === 'user/message' || event.type === 'steering/message')
|
||||
&& event.data.source.kind === 'user')
|
||||
}
|
||||
|
||||
/** Whether this turn is the current goal's exact admitted round. */
|
||||
function isMatchingGoalRound(execution: GoalToolExecution, goal: GoalView): boolean {
|
||||
return execution.events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'goal'
|
||||
&& event.data.source.goalId === goal.id
|
||||
&& event.data.source.revision === goal.revision
|
||||
&& event.data.source.round === goal.roundsStarted)
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authority originating in a human message accepted by a runtime root.
|
||||
* @param ctx - Context carrying the live agent graph.
|
||||
* @param execution - Authenticated current tool execution.
|
||||
*/
|
||||
export function requireDirectHuman(ctx: Context, execution: GoalToolExecution): void {
|
||||
if (hasDirectHumanInput(ctx, execution)) return
|
||||
reject('this goal operation requires a direct human turn on a top-level agent')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve completion authority from either direct human input or the exact goal round.
|
||||
* @param ctx - Context carrying live agents and goal state.
|
||||
* @param execution - Authenticated current tool execution.
|
||||
* @returns The direct-human or exact-goal-round authority grant.
|
||||
*/
|
||||
export function completionAuthority(ctx: Context, execution: GoalToolExecution): GoalToolAuthority {
|
||||
if (hasDirectHumanInput(ctx, execution)) return { kind: 'direct-human' }
|
||||
const goal = ctx.goals.get(execution.agent)
|
||||
if (goal !== undefined && isMatchingGoalRound(execution, goal)) {
|
||||
return { kind: 'goal-round', goal }
|
||||
}
|
||||
return reject('complete and blocked require a direct human turn or the current goal round')
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 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<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, durable phase, admitted '
|
||||
+ 'round count, cap, and live process-local activation. 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; 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,
|
||||
},
|
||||
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 } }
|
||||
}
|
||||
|
||||
/** 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: {},
|
||||
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 cap; omission uses the goal-domain deployment default.',
|
||||
},
|
||||
},
|
||||
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([{ 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 turn. complete and blocked additionally accept the exact admitted goal '
|
||||
+ 'round. blocked is rejected before the configured minimum round count; the model remains '
|
||||
+ 'responsible for judging that the same condition persisted across those rounds.',
|
||||
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.' },
|
||||
},
|
||||
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)
|
||||
return Promise.resolve([{
|
||||
type: 'text',
|
||||
text: renderGoal(ctx.goals.edit(execution.agent, ref, replacements)),
|
||||
}])
|
||||
}
|
||||
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 === 'pause' || args.action === 'resume') {
|
||||
requireDirectHuman(ctx, execution)
|
||||
const goal = args.action === 'pause'
|
||||
? ctx.goals.pause(execution.agent, ref)
|
||||
: ctx.goals.resume(execution.agent, ref)
|
||||
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
||||
}
|
||||
const authority = completionAuthority(ctx, execution)
|
||||
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)
|
||||
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.objective ?? args.goal_id,
|
||||
),
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user