fix(tool-goal): let the model deliver a wrap-up message after a goal-round complete/blocked
A goal round reporting complete or blocked used to conclude the turn at the tool result, so the model never spoke after the call and sessions ended on a bare update_goal card. The terminal update now defers one plugin-sourced <goal_complete>/<goal_blocked> instruction onto its result asking for a grounded closing message without further tool calls; the turn then ends through the ordinary no-tool-calls stop. Direct-human mutations stay uninstructed. Wording chosen by A/B sampling on deepseek-v4-pro; one extra request per goal lifecycle. New keyless ACP snapshot goal-wrapup drives the shipped app through create -> round one -> autonomous complete and pins the injection, the same-turn closing message, and the completed turn end.
This commit is contained in:
@@ -347,7 +347,7 @@ describe('goal tool state transitions', () => {
|
||||
expect(goal).toMatchObject({ phase: 'active', revision: 4 })
|
||||
})
|
||||
|
||||
it('terminal-stops an autonomous completion but leaves a human pause interactive', async () => {
|
||||
it('injects one wrap-up instruction for an autonomous completion but leaves a human pause interactive', async () => {
|
||||
const { ctx, root } = await harness()
|
||||
const humanTurn = openTurn(root, { kind: 'user' })
|
||||
const created = ctx.goals.create(root.agent, { objective: 'pause cleanly' })
|
||||
@@ -356,6 +356,7 @@ describe('goal tool state transitions', () => {
|
||||
}, root.agent)
|
||||
expect(resultGoal(paused)).toMatchObject({ phase: 'paused' })
|
||||
expect(paused.concludesTurn).toBeUndefined()
|
||||
expect(paused.additionalContexts).toBeUndefined()
|
||||
const resumed = resultGoal(await execute(ctx, 'update_goal', {
|
||||
goal_id: created.id, revision: 2, action: 'resume',
|
||||
}, root.agent))
|
||||
@@ -368,7 +369,27 @@ describe('goal tool state transitions', () => {
|
||||
goal_id: created.id, revision: resumed['revision'], action: 'complete',
|
||||
}, root.agent)
|
||||
expect(resultGoal(complete)).toMatchObject({ phase: 'complete' })
|
||||
expect(complete.concludesTurn).toBe(true)
|
||||
expect(complete.concludesTurn).toBeUndefined()
|
||||
const contexts = complete.additionalContexts ?? []
|
||||
expect(contexts).toHaveLength(1)
|
||||
expect(contexts[0]?.source).toEqual({ kind: 'plugin', plugin: 'tool-goal' })
|
||||
const block = contexts[0]?.content[0]
|
||||
if (block?.type !== 'text') throw new Error('expected one text wrap-up block')
|
||||
expect(block.text).toContain('<goal_complete>')
|
||||
expect(block.text).toContain('"pause cleanly"')
|
||||
expect(block.text).toContain('Do not call any more tools.')
|
||||
})
|
||||
|
||||
it('completes without a wrap-up instruction under direct human authority', async () => {
|
||||
const { ctx, root } = await harness()
|
||||
openTurn(root, { kind: 'user' })
|
||||
const created = ctx.goals.create(root.agent, { objective: 'finish now' })
|
||||
const complete = await execute(ctx, 'update_goal', {
|
||||
goal_id: created.id, revision: created.revision, action: 'complete',
|
||||
}, root.agent)
|
||||
expect(resultGoal(complete)).toMatchObject({ phase: 'complete' })
|
||||
expect(complete.concludesTurn).toBeUndefined()
|
||||
expect(complete.additionalContexts).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rearms a restored active goal only after a new direct human prompt', async () => {
|
||||
@@ -550,6 +571,14 @@ describe('goal tool state transitions', () => {
|
||||
blockedReason: { code: 'model-reported', message: 'The required credential is still unavailable.' },
|
||||
roundsStarted: 3,
|
||||
})
|
||||
expect(blocked.concludesTurn).toBeUndefined()
|
||||
const contexts = blocked.additionalContexts ?? []
|
||||
expect(contexts).toHaveLength(1)
|
||||
const block = contexts[0]?.content[0]
|
||||
if (block?.type !== 'text') throw new Error('expected one text wrap-up block')
|
||||
expect(block.text).toContain('<goal_blocked>')
|
||||
expect(block.text).toContain('The required credential is still unavailable.')
|
||||
expect(block.text).toContain('Do not call any more tools.')
|
||||
})
|
||||
|
||||
it('lets direct human authority block before the model threshold', async () => {
|
||||
|
||||
Reference in New Issue
Block a user