refactor(goal): loosen executor presence checks

This commit is contained in:
Turtle
2026-07-24 13:53:06 +08:00
parent d94a4916f1
commit d3e24356ba
2 changed files with 20 additions and 23 deletions

View File

@@ -132,14 +132,14 @@ function resolveConfig(config: Config): ResolvedConfig {
return { blockedAfterConsecutiveRounds: blockedAfter } return { blockedAfterConsecutiveRounds: blockedAfter }
} }
/** Remove only empty provider fillers from fields unused by the selected action. */ /** Whether an optional string carries a meaningful action-specific value. */
function normalizeUpdateArgs(args: Record<string, unknown>): Record<string, unknown> { function hasText(value: string | undefined): boolean {
const action = args['action'] return value !== undefined && value !== ''
const empty = (value: unknown): boolean => value === undefined || value === null || value === '' || value === 0 }
const unused = (key: string): boolean => key === 'blocked_reason'
? action !== 'blocked' /** Whether an optional round cap carries a meaningful action-specific value. */
: (key === 'objective' || key === 'max_goal_rounds') && action !== 'edit' function hasRoundCap(value: number | undefined): boolean {
return Object.fromEntries(Object.entries(args).filter(([key, value]) => !unused(key) || !empty(value))) return value !== undefined && value !== 0
} }
/** Build the exact compare-and-set ref from model arguments. */ /** Build the exact compare-and-set ref from model arguments. */
@@ -254,7 +254,7 @@ export function apply(ctx: Context, config: Config): void {
presentCall: args => present('Create goal', 'other', args.objective), presentCall: args => present('Create goal', 'other', args.objective),
})) }))
const updateGoal = defineTool({ ctx.tools.register(defineTool({
name: 'update_goal', name: 'update_goal',
description: 'Update the exact current goal revision. edit, pause, and resume require a direct ' 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 ' + 'top-level human request. During an automatic continuation of the current goal, complete '
@@ -286,7 +286,9 @@ export function apply(ctx: Context, config: Config): void {
} }
if (args.action === 'edit') { if (args.action === 'edit') {
requireDirectHuman(ctx, execution) requireDirectHuman(ctx, execution)
if (args.blocked_reason !== undefined) { // Some strict-schema providers emit empty placeholders for every declared
// optional field. Reject only values that could change another action.
if (hasText(args.blocked_reason)) {
throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE') throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE')
} }
const goal = ctx.goals.edit(execution.agent, ref, replacements) const goal = ctx.goals.edit(execution.agent, ref, replacements)
@@ -295,7 +297,7 @@ export function apply(ctx: Context, config: Config): void {
} }
if (args.action === 'pause' || args.action === 'resume') { if (args.action === 'pause' || args.action === 'resume') {
requireDirectHuman(ctx, execution) requireDirectHuman(ctx, execution)
if (args.objective !== undefined || args.max_goal_rounds !== undefined || args.blocked_reason !== undefined) { if (hasText(args.objective) || hasRoundCap(args.max_goal_rounds) || hasText(args.blocked_reason)) {
throw new HarnessError( throw new HarnessError(
'objective and max_goal_rounds are valid only with action edit; blocked_reason is valid only with action blocked', 'objective and max_goal_rounds are valid only with action edit; blocked_reason is valid only with action blocked',
'GOAL_TOOL_INVALID_UPDATE', 'GOAL_TOOL_INVALID_UPDATE',
@@ -308,13 +310,13 @@ export function apply(ctx: Context, config: Config): void {
return Promise.resolve(goalValue(goal)) return Promise.resolve(goalValue(goal))
} }
const authority = completionAuthority(ctx, execution) const authority = completionAuthority(ctx, execution)
if (args.objective !== undefined || args.max_goal_rounds !== undefined) { if (hasText(args.objective) || hasRoundCap(args.max_goal_rounds)) {
throw new HarnessError( throw new HarnessError(
'objective and max_goal_rounds are valid only with action edit', 'objective and max_goal_rounds are valid only with action edit',
'GOAL_TOOL_INVALID_UPDATE', 'GOAL_TOOL_INVALID_UPDATE',
) )
} }
if (args.action === 'complete' && args.blocked_reason !== undefined) { if (args.action === 'complete' && hasText(args.blocked_reason)) {
throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE') throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE')
} }
if (args.action === 'blocked' if (args.action === 'blocked'
@@ -343,10 +345,5 @@ export function apply(ctx: Context, config: Config): void {
'other', 'other',
args.blocked_reason ?? args.objective ?? args.goal_id, args.blocked_reason ?? args.objective ?? args.goal_id,
), ),
}) }))
// Object-literal execute methods do not use `this`; retaining the reference is safe.
// eslint-disable-next-line @typescript-eslint/unbound-method
const executeUpdate = updateGoal.execute
updateGoal.execute = (args, exec) => executeUpdate(normalizeUpdateArgs(args as Record<string, unknown>), exec)
ctx.tools.register(updateGoal)
} }

View File

@@ -444,7 +444,7 @@ describe('goal tool state transitions', () => {
action: 'pause', action: 'pause',
objective: '', objective: '',
max_goal_rounds: 0, max_goal_rounds: 0,
blocked_reason: null, blocked_reason: '',
}, root.agent) }, root.agent)
expect(resultGoal(paused)).toMatchObject({ phase: 'paused', objective: 'edited' }) expect(resultGoal(paused)).toMatchObject({ phase: 'paused', objective: 'edited' })
goal = ctx.goals.get(root.agent)! goal = ctx.goals.get(root.agent)!
@@ -453,8 +453,8 @@ describe('goal tool state transitions', () => {
goal_id: goal.id, goal_id: goal.id,
revision: goal.revision, revision: goal.revision,
action: 'resume', action: 'resume',
objective: null, objective: '',
max_goal_rounds: '', max_goal_rounds: 0,
blocked_reason: '', blocked_reason: '',
}, root.agent) }, root.agent)
expect(resultGoal(resumed)).toMatchObject({ phase: 'active', objective: 'edited' }) expect(resultGoal(resumed)).toMatchObject({ phase: 'active', objective: 'edited' })
@@ -465,7 +465,7 @@ describe('goal tool state transitions', () => {
revision: goal.revision, revision: goal.revision,
action: 'blocked', action: 'blocked',
objective: '', objective: '',
max_goal_rounds: null, max_goal_rounds: 0,
blocked_reason: 'actual blocker', blocked_reason: 'actual blocker',
}, root.agent) }, root.agent)
expect(resultGoal(blocked)).toMatchObject({ phase: 'blocked' }) expect(resultGoal(blocked)).toMatchObject({ phase: 'blocked' })