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:
@@ -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(
|
||||
|
||||
40
packages/goal/tool-goal/src/wrapup.ts
Normal file
40
packages/goal/tool-goal/src/wrapup.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/** Model-visible wrap-up instruction for a terminal autonomous goal update. */
|
||||
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
const GROUNDING =
|
||||
'Report only what earlier rounds and tool results in this session actually establish; '
|
||||
+ 'when a detail is not in the session, say so instead of inventing it. '
|
||||
|
||||
/**
|
||||
* Render the closing-message instruction injected after an autonomous goal
|
||||
* round reports `complete` or `blocked`, replacing the former hard turn stop
|
||||
* so the model still addresses the user once before the turn ends.
|
||||
* @param objective - the terminal goal's objective, echoed for grounding.
|
||||
* @param blockedReason - the validated report for `blocked`; omitted for `complete`.
|
||||
* @returns a fresh one-block context for `ToolRunContext.deferContext()`.
|
||||
*/
|
||||
export function renderWrapupContext(objective: string, blockedReason?: string): ContentBlock[] {
|
||||
const heading = `Objective: ${JSON.stringify(objective)}\n`
|
||||
const text = blockedReason === undefined
|
||||
? '<goal_complete>\n'
|
||||
+ heading
|
||||
+ 'The goal is marked complete and this autonomous run is ending. Write the closing '
|
||||
+ 'message to the user now: state the outcome, summarize what was done and how it was '
|
||||
+ 'verified, and point to the concrete results (files, commits, or other artifacts). '
|
||||
+ GROUNDING
|
||||
+ 'Note anything the user should review or do next. Address the user directly. Do not '
|
||||
+ 'call any more tools.\n'
|
||||
+ '</goal_complete>'
|
||||
: '<goal_blocked>\n'
|
||||
+ heading
|
||||
+ `Blocked: ${JSON.stringify(blockedReason)}\n`
|
||||
+ 'The goal is marked blocked and this autonomous run is ending. Write the closing '
|
||||
+ 'message to the user now: state what has been completed so far, describe the concrete '
|
||||
+ 'blocking condition and what you tried, and say exactly what you need from the user to '
|
||||
+ 'continue. '
|
||||
+ GROUNDING
|
||||
+ 'Address the user directly. Do not call any more tools.\n'
|
||||
+ '</goal_blocked>'
|
||||
return [{ type: 'text', text }]
|
||||
}
|
||||
Reference in New Issue
Block a user