fix(goal): preserve interactive human turns
This commit is contained in:
@@ -62,7 +62,11 @@ export function goalToolExecution(ctx: Context, exec: ToolRunContext): GoalToolE
|
||||
return { agent, ...openTurn(agent) }
|
||||
}
|
||||
|
||||
/** Whether an accepted human message appears in the current root-agent turn. */
|
||||
/**
|
||||
* Whether host-attested human input appears in the current root-agent turn.
|
||||
* An omitted `Agent.send()` / `steer()` source resolves to `user`, so non-human
|
||||
* producers must supply their own source rather than inheriting this authority.
|
||||
*/
|
||||
function hasDirectHumanInput(ctx: Context, execution: GoalToolExecution): boolean {
|
||||
if (!ctx.agents.roots().includes(execution.agent)) return false
|
||||
return execution.events.some(event =>
|
||||
|
||||
@@ -50,8 +50,8 @@ const CREATE_DESCRIPTION =
|
||||
+ '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.'
|
||||
'Read the current same-session goal, including its exact id/revision, objective, phase, completed '
|
||||
+ 'continuation rounds, round limit, 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 {
|
||||
@@ -107,13 +107,14 @@ function present(title: string, kind: 'read' | 'other', rawInput?: unknown): Gen
|
||||
return { card: 'generic', title, kind, ...rawInput === undefined ? {} : { rawInput } }
|
||||
}
|
||||
|
||||
/** Remember whether one successful mutation makes this turn terminal. */
|
||||
/** Remember whether one autonomous terminal report should stop this turn. */
|
||||
function observeMutation(
|
||||
terminalTurns: WeakMap<Agent, number>,
|
||||
execution: GoalToolExecution,
|
||||
goal: GoalView,
|
||||
autonomousTerminal: boolean,
|
||||
): void {
|
||||
if (goal.phase === 'active' && goal.activation === 'armed') {
|
||||
if (!autonomousTerminal || (goal.phase === 'active' && goal.activation === 'armed')) {
|
||||
terminalTurns.delete(execution.agent)
|
||||
return
|
||||
}
|
||||
@@ -123,6 +124,8 @@ function observeMutation(
|
||||
/** 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
|
||||
@@ -160,7 +163,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
},
|
||||
max_goal_rounds: {
|
||||
type: 'number',
|
||||
description: 'Optional positive safe-integer cap; omission uses the goal-domain deployment default.',
|
||||
description: 'Optional positive safe-integer limit on automatic continuation rounds.',
|
||||
},
|
||||
},
|
||||
execute(args, exec) {
|
||||
@@ -170,7 +173,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
objective: args.objective,
|
||||
...args.max_goal_rounds === undefined ? {} : { maxGoalRounds: args.max_goal_rounds },
|
||||
})
|
||||
observeMutation(terminalTurns, execution, goal)
|
||||
observeMutation(terminalTurns, execution, goal, false)
|
||||
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
||||
},
|
||||
presentCall: args => present('Create goal', 'other', args.objective),
|
||||
@@ -179,8 +182,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
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 '
|
||||
+ '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.',
|
||||
parameters: {
|
||||
goal_id: { type: 'string', required: true, description: 'Exact id returned by get_goal.' },
|
||||
@@ -204,27 +207,33 @@ export function apply(ctx: Context, config: Config): void {
|
||||
if (args.action === 'edit') {
|
||||
requireDirectHuman(ctx, execution)
|
||||
const goal = ctx.goals.edit(execution.agent, ref, replacements)
|
||||
observeMutation(terminalTurns, execution, goal)
|
||||
observeMutation(terminalTurns, execution, goal, 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) {
|
||||
throw new HarnessError(
|
||||
'objective and max_goal_rounds are valid only with action edit',
|
||||
'GOAL_TOOL_INVALID_UPDATE',
|
||||
)
|
||||
}
|
||||
const goal = args.action === 'pause'
|
||||
? ctx.goals.pause(execution.agent, ref)
|
||||
: ctx.goals.resume(execution.agent, ref)
|
||||
observeMutation(terminalTurns, execution, goal, 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 === 'pause' || args.action === 'resume') {
|
||||
requireDirectHuman(ctx, execution)
|
||||
const goal = args.action === 'pause'
|
||||
? ctx.goals.pause(execution.agent, ref)
|
||||
: ctx.goals.resume(execution.agent, ref)
|
||||
observeMutation(terminalTurns, execution, goal)
|
||||
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(
|
||||
@@ -236,7 +245,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const goal = args.action === 'complete'
|
||||
? ctx.goals.complete(execution.agent, ref)
|
||||
: ctx.goals.block(execution.agent, ref)
|
||||
observeMutation(terminalTurns, execution, goal)
|
||||
observeMutation(terminalTurns, execution, goal, authority.kind === 'goal-round')
|
||||
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
||||
},
|
||||
presentCall: args => present(
|
||||
|
||||
Reference in New Issue
Block a user