mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(goal): accept empty update fillers
This commit is contained in:
@@ -132,6 +132,32 @@ function resolveConfig(config: Config): ResolvedConfig {
|
||||
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. */
|
||||
function goalRef(goalId: string, revision: number): GoalRef {
|
||||
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),
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
const updateGoal = defineTool({
|
||||
name: 'update_goal',
|
||||
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 '
|
||||
@@ -333,5 +359,10 @@ export function apply(ctx: Context, config: Config): void {
|
||||
'other',
|
||||
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')
|
||||
})
|
||||
|
||||
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 () => {
|
||||
const { ctx, root } = await harness()
|
||||
const humanTurn = openTurn(root, { kind: 'user' })
|
||||
|
||||
Reference in New Issue
Block a user