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:
ZiyaZhang
2026-08-02 05:58:45 -07:00
parent 04d43e3b19
commit 666ef95f81
19 changed files with 334 additions and 19 deletions

View File

@@ -8,7 +8,7 @@ import type { Context } from 'cordis'
import z from 'schemastery'
import { GoalId } from '@deepseek-ai/dsh-goal'
import type { GoalRef, GoalView } from '@deepseek-ai/dsh-goal'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import { createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { GenericCallView } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-system-prompt'
@@ -17,6 +17,7 @@ import {
goalToolExecution,
requireDirectHuman,
} from './authority.ts'
import { renderWrapupContext } from './wrapup.ts'
export const name = 'tool-goal'
export const inject = ['agents', 'goals', 'tools', 'systemPrompt']
@@ -309,7 +310,14 @@ export function apply(ctx: Context, config: Config): void {
code: 'model-reported',
message: args.blocked_reason as string,
})
if (authority.kind === 'goal-round') exec.concludeTurn()
if (authority.kind === 'goal-round') {
exec.deferContext(createUserMessage({
content: args.action === 'complete'
? renderWrapupContext(goal.objective)
: renderWrapupContext(goal.objective, args.blocked_reason as string),
source: { kind: 'plugin', plugin: 'tool-goal' },
}))
}
return Promise.resolve(goalValue(goal))
},
presentCall: args => present(