refactor(goal): loosen executor presence checks
This commit is contained in:
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
Reference in New Issue
Block a user