fix(goal): accept empty update fillers
This commit is contained in:
@@ -132,6 +132,32 @@ function resolveConfig(config: Config): ResolvedConfig {
|
|||||||
return { blockedAfterConsecutiveRounds: blockedAfter }
|
return { blockedAfterConsecutiveRounds: blockedAfter }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Remove only empty provider fillers from fields unused by the selected action. */
|
||||||
|
function normalizeUpdateArgs(args: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const normalized = { ...args }
|
||||||
|
const empty = (value: unknown): boolean => value === undefined || value === null || value === '' || value === 0
|
||||||
|
const removeEmpty = (key: string): void => {
|
||||||
|
if (empty(normalized[key])) normalized[key] = undefined
|
||||||
|
}
|
||||||
|
switch (normalized['action']) {
|
||||||
|
case 'edit':
|
||||||
|
removeEmpty('blocked_reason')
|
||||||
|
break
|
||||||
|
case 'blocked':
|
||||||
|
removeEmpty('objective')
|
||||||
|
removeEmpty('max_goal_rounds')
|
||||||
|
break
|
||||||
|
case 'pause':
|
||||||
|
case 'resume':
|
||||||
|
case 'complete':
|
||||||
|
removeEmpty('objective')
|
||||||
|
removeEmpty('max_goal_rounds')
|
||||||
|
removeEmpty('blocked_reason')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
/** Build the exact compare-and-set ref from model arguments. */
|
/** Build the exact compare-and-set ref from model arguments. */
|
||||||
function goalRef(goalId: string, revision: number): GoalRef {
|
function goalRef(goalId: string, revision: number): GoalRef {
|
||||||
if (goalId.length === 0 || goalId !== goalId.trim()
|
if (goalId.length === 0 || goalId !== goalId.trim()
|
||||||
@@ -244,7 +270,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),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
ctx.tools.register(defineTool({
|
const updateGoal = 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 '
|
||||||
@@ -333,5 +359,10 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,6 +423,65 @@ describe('goal tool state transitions', () => {
|
|||||||
expect(malformedRef.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE')
|
expect(malformedRef.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('accepts only empty fillers in fields unused by the selected action', async () => {
|
||||||
|
const { ctx, root } = await harness()
|
||||||
|
openTurn(root, { kind: 'user' })
|
||||||
|
let goal = ctx.goals.create(root.agent, { objective: 'valid' })
|
||||||
|
|
||||||
|
const edited = await execute(ctx, 'update_goal', {
|
||||||
|
goal_id: goal.id,
|
||||||
|
revision: goal.revision,
|
||||||
|
action: 'edit',
|
||||||
|
objective: 'edited',
|
||||||
|
blocked_reason: '',
|
||||||
|
}, root.agent)
|
||||||
|
expect(resultGoal(edited)).toMatchObject({ objective: 'edited' })
|
||||||
|
goal = ctx.goals.get(root.agent)!
|
||||||
|
|
||||||
|
const paused = await execute(ctx, 'update_goal', {
|
||||||
|
goal_id: goal.id,
|
||||||
|
revision: goal.revision,
|
||||||
|
action: 'pause',
|
||||||
|
objective: '',
|
||||||
|
max_goal_rounds: 0,
|
||||||
|
blocked_reason: null,
|
||||||
|
}, root.agent)
|
||||||
|
expect(resultGoal(paused)).toMatchObject({ phase: 'paused', objective: 'edited' })
|
||||||
|
goal = ctx.goals.get(root.agent)!
|
||||||
|
|
||||||
|
const resumed = await execute(ctx, 'update_goal', {
|
||||||
|
goal_id: goal.id,
|
||||||
|
revision: goal.revision,
|
||||||
|
action: 'resume',
|
||||||
|
objective: null,
|
||||||
|
max_goal_rounds: '',
|
||||||
|
blocked_reason: '',
|
||||||
|
}, root.agent)
|
||||||
|
expect(resultGoal(resumed)).toMatchObject({ phase: 'active', objective: 'edited' })
|
||||||
|
goal = ctx.goals.get(root.agent)!
|
||||||
|
|
||||||
|
const blocked = await execute(ctx, 'update_goal', {
|
||||||
|
goal_id: goal.id,
|
||||||
|
revision: goal.revision,
|
||||||
|
action: 'blocked',
|
||||||
|
objective: '',
|
||||||
|
max_goal_rounds: null,
|
||||||
|
blocked_reason: 'actual blocker',
|
||||||
|
}, root.agent)
|
||||||
|
expect(resultGoal(blocked)).toMatchObject({ phase: 'blocked' })
|
||||||
|
goal = ctx.goals.resume(root.agent, { id: goal.id, revision: goal.revision + 1 })
|
||||||
|
|
||||||
|
const complete = await execute(ctx, 'update_goal', {
|
||||||
|
goal_id: goal.id,
|
||||||
|
revision: goal.revision,
|
||||||
|
action: 'complete',
|
||||||
|
objective: '',
|
||||||
|
max_goal_rounds: 0,
|
||||||
|
blocked_reason: '',
|
||||||
|
}, root.agent)
|
||||||
|
expect(resultGoal(complete)).toMatchObject({ phase: 'complete', objective: 'edited' })
|
||||||
|
})
|
||||||
|
|
||||||
it('allows exact goal rounds to complete but not edit or pause', async () => {
|
it('allows exact goal rounds to complete but not edit or pause', async () => {
|
||||||
const { ctx, root } = await harness()
|
const { ctx, root } = await harness()
|
||||||
const humanTurn = openTurn(root, { kind: 'user' })
|
const humanTurn = openTurn(root, { kind: 'user' })
|
||||||
|
|||||||
Reference in New Issue
Block a user