fix(goal): close human command surface gaps
This commit is contained in:
@@ -12,7 +12,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
|
||||
|---|---|
|
||||
| `@deepseek-ai/dsh-agent-spine-demo` | the spine, pre-creating a `main` agent from this app's provider/model pair with `process.cwd()` as the fresh session cwd and carrying its `persona` |
|
||||
| `@deepseek-ai/dsh-commands` | the human-command registry consumed by the TUI front door and optional command plugins |
|
||||
| `@deepseek-ai/dsh-command-goal` | the direct `/goal` producer; the app enables the spine's persisted-goal stack with it |
|
||||
| `@deepseek-ai/dsh-command-goal` | the direct `/goal` producer mounted only for the TUI front door; readline retains the model-mediated goal path |
|
||||
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
|
||||
| `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool |
|
||||
@@ -38,7 +38,7 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
|
||||
| `skills` | owner defaults | registry-cache, local-provider, and model-facing skill-tool config, routed through `dsh-agent-spine-demo` |
|
||||
| `toolBash` | owner defaults | model-facing bash config routed through `dsh-agent-spine-demo`, including bash's producer-local `enableRunInBackground` |
|
||||
| `toolTasks` | owner defaults | generic `task_output` wait bounds routed through `dsh-agent-spine-demo` |
|
||||
| `goals` | owner defaults | persisted goal-domain and model-tool config; `false` removes the goal stack and `/goal` producer |
|
||||
| `goals` | owner defaults | persisted goal-domain and model-tool config; `false` removes the goal stack and the TUI `/goal` producer |
|
||||
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
||||
| `welcome` | `ready.` | terminal banner / TUI subtitle |
|
||||
| `ui` | `{ mode: 'auto' }` | terminal mode (`auto` / `readline` / `tui`) and nested TUI presentation config |
|
||||
|
||||
@@ -101,7 +101,7 @@ export interface Config {
|
||||
toolBash?: NonNullable<agentCore.Config['toolBash']>
|
||||
/** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */
|
||||
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
|
||||
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and command. */
|
||||
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and TUI command. */
|
||||
goals?: agentCore.GoalConfig | false
|
||||
/**
|
||||
* If set, the pre-created agent RESUMES this persisted session id instead of
|
||||
@@ -152,7 +152,7 @@ export function composeTerminalApp(ctx: Context, config: Config, isTTY: boolean)
|
||||
const goals = config.goals ?? {}
|
||||
if (mode === 'readline') ctx.plugin(ConsoleExporter)
|
||||
ctx.plugin(CommandService)
|
||||
if (goals !== false) ctx.plugin(commandGoal)
|
||||
if (mode === 'tui' && goals !== false) ctx.plugin(commandGoal)
|
||||
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT })
|
||||
ctx.plugin(UserInteractionService)
|
||||
if (mode === 'tui') {
|
||||
|
||||
@@ -118,17 +118,25 @@ describe('dsh-stdio-demo app', () => {
|
||||
|
||||
calls.length = 0
|
||||
stdioAgent.composeTerminalApp(ctx, {
|
||||
provider: 'mock', model: 'mock', workspaceContext: false, goals: false, ui: { mode: 'readline' },
|
||||
provider: 'mock', model: 'mock', workspaceContext: false, goals: false, ui: { mode: 'tui' },
|
||||
}, true)
|
||||
expect(calls.map(call => call.name)).toContain('ui-tui')
|
||||
expect(calls.map(call => call.name)).not.toContain('command-goal')
|
||||
expect(calls.find(call => call.name === 'agent-spine-demo')?.config).toMatchObject({ goals: false })
|
||||
|
||||
calls.length = 0
|
||||
stdioAgent.composeTerminalApp(ctx, {
|
||||
provider: 'mock', model: 'mock', workspaceContext: false, ui: { mode: 'readline' },
|
||||
}, false)
|
||||
expect(calls.map(call => call.name)).toContain('ui-stdio')
|
||||
expect(calls.map(call => call.name)).toContain('ConsoleExporter')
|
||||
expect(calls.map(call => call.name)).not.toContain('ui-tui')
|
||||
expect(calls.map(call => call.name)).not.toContain('command-goal')
|
||||
expect(calls.find(call => call.name === 'agent-spine-demo')?.config).toMatchObject({ goals: false })
|
||||
expect(calls.find(call => call.name === 'agent-spine-demo')?.config).toMatchObject({ goals: {} })
|
||||
})
|
||||
|
||||
it('composes the spine + front-door cluster and pre-creates the main agent', async () => {
|
||||
const ctx = await mount({ provider: 'mock', model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-demo-spec', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
const ctx = await mount({ provider: 'mock', model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-demo-spec', skills: await isolatedSkillsConfig(), workspaceContext: false, ui: { mode: 'readline' } })
|
||||
// The spine services (brought up by the agent-spine-demo bundle) are all present.
|
||||
expect(ctx.get('agents')).toBeDefined()
|
||||
expect(ctx.get('agentLoop')).toBeDefined()
|
||||
@@ -145,7 +153,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
expect(agent?.id).toBe(agent?.session.id)
|
||||
expect(agent?.id).toMatch(/^main-session-/)
|
||||
expect(agent?.session.header.cwd).toBe(process.cwd())
|
||||
expect(ctx.commands.find(agent!, 'tui', 'goal')).toBeDefined()
|
||||
expect(ctx.commands.find(agent!, 'tui', 'goal')).toBeUndefined()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Human-facing `/goal` control over [`ctx.goals`](../goal/README.md). The plugin r
|
||||
|
||||
Control words are case-insensitive only when they occupy the complete input. Every other non-empty suffix is an objective, so `/goal pause after verification` creates that literal objective. The goal domain trims and validates objectives. Because the generic command plane has no modal editor or confirmation primitive, `edit` takes its replacement inline and an unfinished replacement returns a direct error instructing the user to edit or clear.
|
||||
|
||||
Expected domain rejections become direct command errors. Unexpected implementation failures still reject dispatch so adapters can report them as command failures. Generic command text and output remain live UI state; every accepted mutation is persisted and made model-visible by `dsh-goal` rather than by this plugin.
|
||||
Expected domain rejections become stable direct command errors without exposing branded ids or revisions. Unexpected implementation failures still reject dispatch so adapters can report them as command failures. Generic command text and output remain live UI state; every accepted mutation is persisted and made model-visible by `dsh-goal` rather than by this plugin.
|
||||
|
||||
## Composition
|
||||
|
||||
@@ -30,7 +30,7 @@ The producer injects `commands` and `goals`. A custom app mounts their owners pl
|
||||
name: '@deepseek-ai/dsh-command-goal'
|
||||
```
|
||||
|
||||
The terminal and ACP demo apps enable the complete persisted-goal stack and this command by default; `goals: false` removes both. The UI-less `agent-spine-demo` requires an explicit `goals: {}` so headless one-shot callers do not silently change from one physical turn to a multi-round operation.
|
||||
The TUI and ACP demo apps enable the complete persisted-goal stack and this command by default; `goals: false` removes both. The terminal app's readline mode keeps the model-mediated goal stack but does not mount this producer because that front door does not consume commands. The UI-less `agent-spine-demo` requires an explicit `goals: {}` so headless one-shot callers do not silently change from one physical turn to a multi-round operation.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ function commandHint(goal: GoalView): string {
|
||||
case 'usage-limited':
|
||||
return '/goal edit <objective>, /goal resume, /goal clear'
|
||||
case 'budget-limited':
|
||||
return '/goal edit <objective>, /goal clear'
|
||||
return '/goal edit <objective>, /goal clear; after the agent raises the round cap, /goal resume'
|
||||
case 'complete':
|
||||
return '/goal <objective>, /goal clear'
|
||||
/* v8 ignore next 2 -- the active branch and every non-active phase are handled above */
|
||||
@@ -149,7 +149,12 @@ function executeGoalCommand(ctx: Context, invocation: CommandInvocation): Comman
|
||||
default: return assertNever(command, 'goal command')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof GoalError) return { kind: 'error', text: error.message }
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,8 +197,10 @@ describe('/goal human command', () => {
|
||||
const test = await harness()
|
||||
await run(test, ' work')
|
||||
const redundantResume = await run(test, ' RESUME')
|
||||
expect(redundantResume.kind).toBe('error')
|
||||
expect(redundantResume.text).toContain('already active and armed')
|
||||
expect(redundantResume).toEqual({
|
||||
kind: 'error',
|
||||
text: 'The goal command is not valid for the current state. Run /goal to view available commands.',
|
||||
})
|
||||
const paused = await run(test, ' PAUSE')
|
||||
expect(paused.kind).toBe('success')
|
||||
expect(paused.text).toContain('Goal paused')
|
||||
@@ -238,7 +240,7 @@ describe('/goal human command', () => {
|
||||
goal = test.ctx.goals.markBudgetLimited(test.agent, ref(goal))
|
||||
const limited = await run(test)
|
||||
expect(limited.text).toContain('Status: limited by round budget')
|
||||
expect(limited.text).not.toContain('/goal resume')
|
||||
expect(limited.text).toContain('after the agent raises the round cap, /goal resume')
|
||||
|
||||
goal = test.ctx.goals.complete(test.agent, ref(goal))
|
||||
const complete = await run(test)
|
||||
|
||||
Reference in New Issue
Block a user