feat(goal): add human goal command

This commit is contained in:
Tianyi Cui
2026-07-19 23:55:33 +08:00
parent 400b4658ef
commit 207692bc16
114 changed files with 1831 additions and 88 deletions

View File

@@ -12,6 +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-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 |
@@ -37,6 +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 |
| `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 |
@@ -82,7 +84,7 @@ Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo —
#### What the model sees
Through `dsh-agent-spine-demo`, the `main` agent receives the harness identity, configured persona, skill catalog, and visible tools; this app also composes the generated [`ask_user_question` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ask-user). Each ordinary terminal submission becomes a user message; submissions made while the agent runs steer the active turn. TUI commands and their results remain outside model context.
Through `dsh-agent-spine-demo`, the `main` agent receives the harness identity, configured persona, skill catalog, visible tools, and the enabled goal policy/tools; this app also composes the generated [`ask_user_question` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ask-user). Each ordinary terminal submission becomes a user message; submissions made while the agent runs steer the active turn. TUI commands and their direct results remain outside model context, while accepted `/goal` mutations append the goal domain's model-visible snapshot.
#### Token effect
@@ -109,5 +111,6 @@ Append-only; newly visible content follows the reusable request prefix and does
## Known Limitations and Deferred Work
- **One pre-created `main` agent drives the selected terminal UI** — there is no multi-session or concurrent-agent surface in this app; a run is one conversation.
- **Direct commands require TUI mode** — the line-oriented fallback does not consume `ctx.commands`; an ordinary `/goal` prompt there may instead be interpreted through the model-facing goal tools.
- **The front-door cluster is fixed in code** — the JSONL persistence backend and the ask-user tooling are baked; a different composition is a leaf-level sibling entry or another app package.
- **The question tool is not an approval answerer** — this app mounts `user-interaction` and `ask_user_question`, but not `ctx.approval`; a `tools/pre-execute` `ask` therefore fails closed unless the leaf composes an approval service and terminal answerer.

View File

@@ -37,6 +37,7 @@
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
"@deepseek-ai/dsh-commands": "^0.0.1",
"@deepseek-ai/dsh-command-goal": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-agent-spine-demo": "^0.0.1",
"@deepseek-ai/dsh-workspace-context": "^0.0.1",
@@ -58,6 +59,7 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-command-goal": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",

View File

@@ -17,6 +17,7 @@ import z from 'schemastery'
import { SessionId } from '@deepseek-ai/dsh-session'
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
import CommandService from '@deepseek-ai/dsh-commands'
import * as commandGoal from '@deepseek-ai/dsh-command-goal'
import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo'
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
@@ -100,6 +101,8 @@ 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. */
goals?: agentCore.GoalConfig | false
/**
* If set, the pre-created agent RESUMES this persisted session id instead of
* starting fresh. Sourced from an env var in the leaf `cordis.yml`
@@ -127,6 +130,7 @@ export const Config: z<Config> = z.object({
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
goals: z.union([z.const(false), agentCore.GoalConfigSchema]),
resumeSessionId: z.string(),
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
})
@@ -145,8 +149,10 @@ export function composeTerminalApp(ctx: Context, config: Config, isTTY: boolean)
const resumeSessionId = config.resumeSessionId === '' ? undefined : config.resumeSessionId
const sessionId = SessionId(resumeSessionId ?? `main-session-${randomUUID()}`)
const mode = resolveTerminalMode(config.ui, isTTY)
const goals = config.goals ?? {}
if (mode === 'readline') ctx.plugin(ConsoleExporter)
ctx.plugin(CommandService)
if (goals !== false) ctx.plugin(commandGoal)
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT })
ctx.plugin(UserInteractionService)
if (mode === 'tui') {
@@ -163,6 +169,7 @@ export function composeTerminalApp(ctx: Context, config: Config, isTTY: boolean)
}
ctx.plugin(agentCore, {
...agentCore.pickSpineConfig(config),
goals,
agents: [{
id: SessionId('main'),
provider: config.provider,

View File

@@ -90,6 +90,7 @@ describe('dsh-stdio-demo app', () => {
ui: { mode: 'tui', tui: { color: false, maxToolOutputLines: 3 } },
}, true)
expect(calls.map(call => call.name)).toContain('ui-tui')
expect(calls.map(call => call.name)).toContain('command-goal')
expect(calls.map(call => call.name)).not.toContain('ui-stdio')
expect(calls.map(call => call.name)).not.toContain('ConsoleExporter')
const tuiConfig = calls.find(call => call.name === 'ui-tui')?.config as { sessionId: string }
@@ -99,6 +100,7 @@ describe('dsh-stdio-demo app', () => {
agents: Array<{ id: string; sessionId?: string; resumeSessionId?: string }>
}
expect(spineConfig.agents[0]).toMatchObject({ id: 'main', sessionId: tuiConfig.sessionId })
expect(spineConfig).toMatchObject({ goals: {} })
calls.length = 0
stdioAgent.composeTerminalApp(ctx, {
@@ -116,11 +118,13 @@ describe('dsh-stdio-demo app', () => {
calls.length = 0
stdioAgent.composeTerminalApp(ctx, {
provider: 'mock', model: 'mock', workspaceContext: false, ui: { mode: 'readline' },
provider: 'mock', model: 'mock', workspaceContext: false, goals: 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 })
})
it('composes the spine + front-door cluster and pre-creates the main agent', async () => {
@@ -131,6 +135,8 @@ describe('dsh-stdio-demo app', () => {
expect(ctx.get('sessionPersistence')).toBeDefined()
expect(ctx.get('userInteraction')).toBeDefined()
expect(ctx.get('tools')?.get('ask_user_question')).toBeDefined()
expect(ctx.get('goals')).toBeDefined()
expect(ctx.get('tools')?.get('get_goal')).toBeDefined()
// The sole pre-created agent the UI drives. `main` is its stable config
// label; each fresh process mints a durable combined agent/session id.
await expect.poll(() => ctx.get('agents')?.list()).toHaveLength(1)
@@ -139,6 +145,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()
await ctx.fiber.dispose()
})
@@ -273,7 +280,18 @@ describe('dsh-stdio-demo app', () => {
})
}
const assembly = await ctx.get('systemPrompt')!.assemble()
expect(assembly.tools.map(tool => tool.name)).toEqual(['zulu', 'alpha', 'ask_user_question', 'skill', 'task_kill', 'task_list', 'task_output'])
expect(assembly.tools.map(tool => tool.name)).toEqual([
'zulu',
'alpha',
'ask_user_question',
'create_goal',
'get_goal',
'skill',
'task_kill',
'task_list',
'task_output',
'update_goal',
])
await ctx.fiber.dispose()
})

View File

@@ -32,6 +32,9 @@
{
"path": "../../ui/commands"
},
{
"path": "../../goal/command-goal"
},
{
"path": "../agent-spine-demo"
},