Files
deepseek-harness/packages/goal/command-goal/src/index.ts
T
creatixchu 8d9fee19f9 feat(commands): route composer image attachments through slash commands
A claimed slash command consumed only the text half of the composer
submission: /goal with reference images executed, cleared the draft, and
silently stranded the images in the rail. Model-visible attachment intent
had no route through the command plane.

The submission envelope is now modeled end to end. CommandDefinition
input.images declares acceptance; the declaration rides the descriptor to
every client, onto the minted CommandClaim, and into the input machine's
claim snapshot. commands.execute carries the submission's base64 images
and enforces the declaration in the executor: non-declaring commands, a
missing attachment store, and exceeded batch limits settle as logged
error results before the handler runs. Admission reuses the attachment
package's new admitEncodedImages, extracted from api-proxy's prompt path
so both wire endpoints share one limits/validation/commit sequence.

Producers own model visibility: /goal submits one user followup (image
blocks + a fixed reference line) after a successful create/edit so goal
rounds read the images from session history; /plan folds them into its
steered message. Grammar misfits (/goal pause, bare /plan, /plan off)
return direct errors and the composer keeps the images.

On the client, enter adjudication carries a SubmitEnvelope and every
command route that cannot consume images throws a localized refusal that
renders as one composer notice with draft and images retained; the
claimed pre-gate applies the same copy. An accepting claim serializes the
draft images, forwards them to commands.execute, and clears plus releases
them only on a success outcome.

The assembled web test roster gains the ui-input-trigger and ui-commands
plugins, mirroring the shipped composition, so slash submissions exercise
the command plane; a new keyless snapshot pins the refusal banner and the
accepting /goal flow over the built client graph.
2026-08-17 18:57:55 +08:00

197 lines
7.9 KiB
TypeScript

/**
* Human-facing `/goal` command over the persisted same-session goal domain.
* @module @deepseek-ai/dsh-command-goal
*/
import type { Context } from '@deepseek-ai/cordis'
import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands'
import { GoalError } from '@deepseek-ai/dsh-goal'
import type { GoalPhase, GoalRef, GoalView } from '@deepseek-ai/dsh-goal'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
export const name = 'command-goal'
export const inject = ['commands', 'goals']
const USAGE = 'Usage: /goal [<objective>|clear|edit <objective>|pause|resume]'
type GoalCommand =
| { readonly kind: 'show' }
| { readonly kind: 'create'; readonly objective: string }
| { readonly kind: 'edit'; readonly objective: string }
| { readonly kind: 'invalid-edit' }
| { readonly kind: 'pause' }
| { readonly kind: 'resume' }
| { readonly kind: 'clear' }
/** Fail loudly if a locally closed union gains an unhandled member. */
/* v8 ignore start -- closed-union backstop is unreachable without violating the TypeScript contract */
function assertNever(value: never, label: string): never {
throw new TypeError(`unknown ${label}: ${String(value)}`)
}
/* v8 ignore stop */
/** Parse only the grammar owned by `/goal`; arbitrary other input is an objective. */
function parseGoalCommand(rawInput: string): GoalCommand {
const input = rawInput.trim()
if (input.length === 0) return { kind: 'show' }
const control = input.toLowerCase()
if (control === 'clear') return { kind: 'clear' }
if (control === 'pause') return { kind: 'pause' }
if (control === 'resume') return { kind: 'resume' }
if (control === 'edit') return { kind: 'invalid-edit' }
if (/^edit(?=\s)/iu.test(input)) return { kind: 'edit', objective: input.slice(4).trim() }
return { kind: 'create', objective: input }
}
/** Human label for one durable goal phase. */
function phaseLabel(phase: GoalPhase): string {
switch (phase) {
case 'active': return 'active'
case 'paused': return 'paused'
case 'blocked': return 'blocked'
case 'complete': return 'complete'
/* v8 ignore next 2 -- GoalPhase is closed and every member is handled above */
default: return assertNever(phase, 'goal phase')
}
}
/** Commands that are meaningful from one exact live state. */
function commandHint(goal: GoalView): string {
if (goal.phase === 'active') {
return goal.activation === 'armed'
? '/goal edit <objective>, /goal pause, /goal clear'
: '/goal edit <objective>, /goal resume, /goal clear'
}
switch (goal.phase) {
case 'paused':
case 'blocked':
return '/goal edit <objective>, /goal resume, /goal clear'
case 'complete':
return '/goal <objective>, /goal clear'
/* v8 ignore next 2 -- the active branch and every non-active phase are handled above */
default: return assertNever(goal.phase, 'goal phase')
}
}
/** Render direct UI output without exposing compare-and-set internals. */
function renderGoal(title: string, goal: GoalView): CommandResult {
const reason = goal.phase === 'blocked' ? goal.blockedReason : undefined
/* v8 ignore next -- durable replay guarantees every blocked goal carries its validated reason */
if (goal.phase === 'blocked' && reason === undefined) throw new TypeError('blocked goal is missing its reason')
const blocker = reason === undefined ? [] : [`Blocker: ${reason.code}: ${reason.message}`]
return {
kind: 'success',
text: [
title,
`Status: ${phaseLabel(goal.phase)}`,
...blocker,
`Objective: ${goal.objective}`,
`Rounds: ${goal.roundsStarted}/${goal.maxGoalRounds}`,
`Activation: ${goal.activation}`,
'',
`Commands: ${commandHint(goal)}`,
].join('\n'),
}
}
/** Exact current compare-and-set ref. */
function goalRef(goal: GoalView): GoalRef {
return { id: goal.id, revision: goal.revision }
}
/** Direct error for an operation that requires a current goal. */
function missingGoal(action: string): CommandResult {
return {
kind: 'error',
text: `No goal is currently set; /goal ${action} requires one. ${USAGE}`,
}
}
/**
* Submit the invocation's admitted composer images as one model-visible user
* message ahead of the goal's next round. The images precede a fixed text
* block naming their role, so a later goal round reads them from ordinary
* session history without the goal domain storing attachment state.
*/
function submitObjectiveAttachments(invocation: CommandInvocation): void {
if (invocation.attachments.length === 0) return
invocation.agent.followup(createUserMessage({
content: [...invocation.attachments, { type: 'text', text: 'Reference images for the goal objective.' }],
source: { kind: 'user' },
}))
}
/** Execute one parsed human command through the domain that owns persistence. */
function executeGoalCommand(ctx: Context, invocation: CommandInvocation): CommandResult {
const command = parseGoalCommand(invocation.rawInput)
if (invocation.attachments.length > 0 && command.kind !== 'create' && command.kind !== 'edit') {
return {
kind: 'error',
text: 'Image attachments only accompany a goal objective: /goal <objective> or /goal edit <objective>.',
}
}
try {
const current = ctx.goals.get(invocation.agent)
switch (command.kind) {
case 'show':
return current === undefined
? { kind: 'success', text: `No goal is currently set.\n${USAGE}` }
: renderGoal('Goal', current)
case 'invalid-edit':
return { kind: 'error', text: `Goal editing requires a replacement objective.\n${USAGE}` }
case 'create': {
if (current !== undefined && current.phase !== 'complete') {
return {
kind: 'error',
text: `A goal is already ${phaseLabel(current.phase)}. Use /goal edit <objective> to change it or /goal clear before replacing it.`,
}
}
const created = ctx.goals.create(invocation.agent, { objective: command.objective })
submitObjectiveAttachments(invocation)
return renderGoal('Goal created', created)
}
case 'edit': {
if (current === undefined) return missingGoal('edit')
if (current.phase === 'complete') {
const replaced = ctx.goals.create(invocation.agent, { objective: command.objective })
submitObjectiveAttachments(invocation)
return renderGoal('Goal created', replaced)
}
const edited = ctx.goals.edit(invocation.agent, goalRef(current), { objective: command.objective })
submitObjectiveAttachments(invocation)
return renderGoal('Goal updated', edited)
}
case 'pause':
if (current === undefined) return missingGoal('pause')
return renderGoal('Goal paused', ctx.goals.pause(invocation.agent, goalRef(current)))
case 'resume':
if (current === undefined) return missingGoal('resume')
return renderGoal('Goal resumed', ctx.goals.resume(invocation.agent, goalRef(current)))
case 'clear':
if (current === undefined) return { kind: 'success', text: 'No goal to clear.' }
ctx.goals.clear(invocation.agent, goalRef(current))
return { kind: 'success', text: 'Goal cleared.' }
/* v8 ignore next 2 -- GoalCommand is closed and every member is handled above */
default: return assertNever(command, 'goal command')
}
} catch (error: unknown) {
if (error instanceof GoalError) {
return {
kind: 'error',
text: 'The goal command is not valid for the current state. Run /goal to view available commands.',
}
}
throw error
}
}
/** Register the Codex-shaped `/goal` command for every composed command adapter. */
export function apply(ctx: Context): void {
ctx.commands.register({
name: 'goal',
description: 'set or view the goal for a long-running task',
input: { hint: '[<objective>|clear|edit <objective>|pause|resume]', images: true },
handler: invocation => executeGoalCommand(ctx, invocation),
})
}