Merge origin/master into codex/invariant-service-seam

# Conflicts:
#	docs/config-catalog.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	packages/examples/agent-spine-demo/README.md
#	packages/examples/agent-spine-demo/package.json
#	packages/examples/agent-spine-demo/src/index.ts
#	packages/examples/tui-demo/package.json
#	packages/support/invariants/src/scoped-events.generated.ts
#	packages/ui/acp/package.json
#	packages/ui/tui/package.json
#	packages/ui/tui/tests/tui.spec.ts
#	pnpm-lock.yaml
This commit is contained in:
Tianyi Cui
2026-07-21 01:13:28 +08:00
213 changed files with 7860 additions and 257 deletions

View File

@@ -4,10 +4,10 @@ Pre-composed plugin bundles a thin leaf `cordis.yml` loads instead of assembling
| Package | npm name | Role |
|---|---|---|
| `agent-spine-demo/` | `@deepseek-ai/dsh-agent-spine-demo` | The executor-less/UI-less agent spine as one bundle plugin (`timer` + `llm` + sessions + system-prompt + tools + skills + agents + invariants + `tool-bash` + workspace-context + `tool-skill` + `agent-loop`) |
| `tui-demo/` | `@deepseek-ai/dsh-tui-demo` | Full-screen terminal app: the spine + JSONL persistence + `dsh-tui` + a pre-created `main` agent, with a boot `bin` |
| `agent-spine-demo/` | `@deepseek-ai/dsh-agent-spine-demo` | The executor-less/UI-less agent spine as one bundle plugin, with an opt-in persisted-goal stack |
| `tui-demo/` | `@deepseek-ai/dsh-tui-demo` | Full-screen terminal app: the spine + persisted goals + `/goal` command + JSONL persistence + `dsh-tui` + a pre-created `main` agent, with a boot `bin` |
| `cli-demo/` | `@deepseek-ai/dsh-cli-demo` | Headless one-shot app: the spine + JSONL persistence + a pre-created `main` agent, with text and DSH-native JSON output |
| `acp-demo/` | `@deepseek-ai/dsh-acp-demo` | ACP server app: the spine + JSONL persistence + the [`acp`](../ui/acp/README.md) bridge (no stdout logger), with a boot `bin` |
| `acp-demo/` | `@deepseek-ai/dsh-acp-demo` | ACP server app: the spine + persisted goals + `/goal` command + JSONL persistence + the [`acp`](../ui/acp/README.md) bridge (no stdout logger), with a boot `bin` |
| `jsonrpc-demo/` | `@deepseek-ai/dsh-jsonrpc-demo` | Bin-only runtime that boots an external `cordis.yml` for the stdio JSON-RPC SDK client |
`agent-spine-demo` is the shared bundle; `tui-demo`, `cli-demo`, and `acp-demo` compose it with full-screen terminal, headless one-shot, and ACP front doors and own their boot bins. `jsonrpc-demo` mounts no composition of its own — it boots whatever tree the deployment's `cordis.yml` names, and is what the Python SDK runtime launches.

View File

@@ -11,6 +11,8 @@ stdout is the ACP JSON-RPC channel, so the cluster is defined as much by what it
| Plugin | Why |
|---|---|
| `@deepseek-ai/dsh-agent-spine-demo` | the spine, pre-creating **no** agents (ACP `session/new` creates them on demand) |
| `@deepseek-ai/dsh-commands` | the human-command registry used for ACP discovery and direct slash dispatch |
| `@deepseek-ai/dsh-command-goal` | the discoverable direct `/goal` producer; the app enables the spine's persisted-goal stack with it |
| `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by clients that can complete ACP elicitation requests |
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log (the bridge advertises `loadSession`) |
| `@deepseek-ai/dsh-acp` | the bridge that owns stdout for JSON-RPC and provides ACP-backed user answers when a leaf explicitly exposes a user-question tool |
@@ -36,6 +38,7 @@ Because the package wires no logger entry, an ACP leaf has **nothing to get wron
| `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 |
| `llmRetry` | owner defaults | bounded transient model-request retry policy routed through `dsh-agent-spine-demo` |
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
| `persistenceCompression` | `'zstd'` | JSONL artifact encoding (`'zstd'` or raw `'none'`) |
@@ -56,7 +59,7 @@ All diagnostics go to **stderr** — stdout is the protocol.
## Model Experience
Indirectly, through `dsh-agent-spine-demo` and `dsh-acp`, which compose each ACP agent's prompt, tools, and message history; this app bundle adds no model-bound content itself.
Indirectly, through `dsh-agent-spine-demo` and `dsh-acp`, which compose each ACP agent's prompt, goal tools, and message history. Direct `/goal` input and output remain outside the model, while accepted mutations append domain-owned model-visible snapshots.
#### KV Cache effect

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-acp-demo",
"description": "ACP server app: the agent-spine-demo bundle + JSONL persistence + the ACP bridge (no stdout logger, no hmr, no pre-created agents), with a bin to boot a leaf cordis.yml over JSON-RPC stdio",
"description": "ACP server app: agent spine + human commands + JSONL persistence + ACP bridge (no stdout logger, hmr, or pre-created agents), with a JSON-RPC stdio bin",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -38,6 +38,8 @@
"@cordisjs/plugin-include": "^1.0.4",
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-acp": "^0.0.1",
"@deepseek-ai/dsh-commands": "^0.0.1",
"@deepseek-ai/dsh-command-goal": "^0.0.1",
"@deepseek-ai/dsh-agent-spine-demo": "^0.0.1",
"@deepseek-ai/dsh-app-boot": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
@@ -52,6 +54,8 @@
"@cordisjs/plugin-include": "workspace:^",
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/dsh-acp": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-command-goal": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",

View File

@@ -1,7 +1,7 @@
/**
* The ACP server app: the default agent spine ({@link @deepseek-ai/dsh-agent-spine-demo}),
* JSONL session persistence, and the {@link @deepseek-ai/dsh-acp} bridge. It
* writes nothing to stdout.
* human-command registry, JSONL session persistence, and the
* {@link @deepseek-ai/dsh-acp} bridge. It writes nothing to stdout.
* It pre-creates no agents and leaves adapters, executors, and optional tools to
* the leaf, which must likewise avoid stdout loggers. Named exports are
* required so Loader retains this plugin's `Config` schema (see
@@ -12,6 +12,8 @@
import type { Context } from 'cordis'
import z from 'schemastery'
import * as acp from '@deepseek-ai/dsh-acp'
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 ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
@@ -60,6 +62,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
/** Bounded transient model-request retry policy forwarded through agent-core. */
llmRetry?: NonNullable<agentCore.Config['llmRetry']>
}
@@ -84,6 +88,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]),
llmRetry: agentCore.LlmRetryConfigSchema,
})
/* jscpd:ignore-end */
@@ -96,7 +101,10 @@ export const Config: z<Config> = z.object({
* from the provider/model pair. No logger, no `hmr` — stdout stays pure.
*/
export function apply(ctx: Context, config: Config): void {
ctx.plugin(agentCore, agentCore.pickSpineConfig(config))
const goals = config.goals ?? {}
ctx.plugin(CommandService)
if (goals !== false) ctx.plugin(commandGoal)
ctx.plugin(agentCore, { ...agentCore.pickSpineConfig(config), goals })
ctx.plugin(UserInteractionService)
ctx.plugin(SessionPersistenceJsonl, {
root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT,

View File

@@ -93,11 +93,30 @@ describe('dsh-acp-demo composition', () => {
expect(ctx.get('agentLoop')).toBeDefined()
expect(ctx.get('userInteraction')).toBeDefined()
expect(ctx.get('tools')?.get('ask_user_question')).toBeUndefined()
expect(ctx.get('goals')).toBeDefined()
expect(ctx.get('tools')?.get('get_goal')).toBeDefined()
// No pre-created agents — ACP session/new creates them on demand.
expect(ctx.get('agents')!.list()).toHaveLength(0)
await ctx.fiber.dispose()
})
it('can explicitly omit the persisted-goal stack and its command', async () => {
const ctx = await mount({
provider: 'mock',
model: 'mock',
goals: false,
workspaceContext: false,
})
expect(ctx.get('goals')).toBeUndefined()
const handle = await ctx.agents.create({
sessionId: 'disabled-goals' as import('@deepseek-ai/dsh-session').SessionId,
agentOptions: { provider: 'mock', model: 'mock' },
})
expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined()
await handle.dispose()
await ctx.fiber.dispose()
})
it('defaults the persistence root when omitted', async () => {
// Exercises the `DEFAULT_PERSISTENCE_ROOT` fallback for a direct-apply caller that
// bypasses the schema's `.default(...)`: call `apply` directly (not via
@@ -195,7 +214,17 @@ describe('dsh-acp-demo composition', () => {
})
}
const assembly = await ctx.get('systemPrompt')!.assemble()
expect(assembly.tools.map(tool => tool.name)).toEqual(['zulu', 'alpha', 'skill', 'task_kill', 'task_list', 'task_output'])
expect(assembly.tools.map(tool => tool.name)).toEqual([
'zulu',
'alpha',
'create_goal',
'get_goal',
'skill',
'task_kill',
'task_list',
'task_output',
'update_goal',
])
await ctx.fiber.dispose()
})

View File

@@ -23,6 +23,12 @@
{
"path": "../../ui/acp"
},
{
"path": "../../ui/commands"
},
{
"path": "../../goal/command-goal"
},
{
"path": "../../core/agent"
},

View File

@@ -17,6 +17,9 @@ Read this package for the whole plugin tree and its composition order.
@deepseek-ai/dsh-skill skill provider registry
@deepseek-ai/dsh-skill-local local filesystem skill provider
@deepseek-ai/dsh-agent agent registry + initiator scope + agent/* events
@deepseek-ai/dsh-goal optional persisted same-session goal domain
@deepseek-ai/dsh-tool-goal optional model-facing goal controls
@deepseek-ai/dsh-goal-session optional same-session goal-round driver
@deepseek-ai/dsh-llm-retry bounded transient request retry policy
@deepseek-ai/dsh-tasks generic background-task registry
@deepseek-ai/dsh-invariants configurable invariant registry service
@@ -48,11 +51,11 @@ This is the [interface/implementation/consumer seam](../../../.agents/notes/impl
```ts
import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
// { agents?, maxParallelToolCalls?, persona?, toolOrder?, tools?, dshHome?, skills?, workspaceContext, toolBash?, toolTasks?, invariants?, llmRetry? }
// { agents?, maxParallelToolCalls?, persona?, toolOrder?, tools?, dshHome?, skills?, workspaceContext, toolBash?, toolTasks?, goals?, invariants?, llmRetry? }
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `llmRetry` to the bounded retry policy; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. It resolves `dshHome` once through [`@deepseek-ai/dsh-home`](../../util/home/README.md) and forwards that absolute value to tool-bash's managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `llmRetry` to the bounded retry policy; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. A `goals` object opts into the persisted domain, model tools, and same-session driver while forwarding `goals.domain` and `goals.tool` to their owners; omission or `false` leaves the stack absent so headless callers retain one-turn settlement. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. It resolves `dshHome` once through [`@deepseek-ai/dsh-home`](../../util/home/README.md) and forwards that absolute value to tool-bash's managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../support/invariants/README.md) for regex and lifecycle rules.
@@ -64,7 +67,7 @@ The bounded retry policy may repeat a transiently failed request in a new number
## Model Experience
Indirectly, through `dsh-system-prompt`, `dsh-tool-skill`, `dsh-tool-bash`, `dsh-tools`, and `dsh-llm-retry`, which this bundle mounts without adding model-bound wrapper content.
Indirectly, through `dsh-system-prompt`, `dsh-tool-skill`, `dsh-tool-bash`, `dsh-tools`, and `dsh-llm-retry`, plus `dsh-tool-goal` and goal-round prompts when `goals` is enabled. The bundle adds no model-bound wrapper content of its own.
#### KV Cache effect
@@ -72,5 +75,5 @@ No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **Most of the spine set is fixed in code** — `apply()` always mounts the core services and `tool-bash`; config can omit the bundled skills and task-control tools, but swapping the loop or dropping another spine member means composing a different bundle.
- **Most of the spine set is fixed in code** — `apply()` always mounts the core services and `tool-bash`; config can omit bundled goals, skills, and task-control tools, but swapping the loop or dropping another spine member means composing a different bundle.
- **The invariant seam and companions remain fixed members** — `invariants.enabled: false` or package filters suppress checks but do not remove the service or companion registrations; Session's always-on validation and freezing are separate.

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-agent-spine-demo",
"description": "The default executor-less/UI-less agent spine as one Cordis bundle plugin (timer + llm + sessions + system-prompt + tools + skills + agents + bounded retry + tasks + invariants + tool-bash + workspace-context + tool-skill + tool-tasks + agent-loop)",
"description": "The default executor-less/UI-less agent spine with bounded retry and optional persisted goals",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -30,6 +30,8 @@
"@cordisjs/plugin-timer": "^1.1.2",
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
"@deepseek-ai/dsh-goal": "^0.0.1",
"@deepseek-ai/dsh-goal-session": "^0.0.1",
"@deepseek-ai/dsh-home": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
@@ -41,6 +43,7 @@
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tasks": "^0.0.1",
"@deepseek-ai/dsh-tool-bash": "^0.0.1",
"@deepseek-ai/dsh-tool-goal": "^0.0.1",
"@deepseek-ai/dsh-tool-skill": "^0.0.1",
"@deepseek-ai/dsh-tool-tasks": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
@@ -51,6 +54,8 @@
"@cordisjs/plugin-timer": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-goal-session": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-home": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
@@ -63,6 +68,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-tool-bash": "workspace:^",
"@deepseek-ai/dsh-tool-goal": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",

View File

@@ -1,6 +1,6 @@
/**
* Default executor-less, UI-less agent spine. It bundles the common services,
* background-task registry and controls, concrete loop, local skill and
* background-task registry and controls, optional persisted goals, concrete loop, local skill and
* workspace-context providers, and model-facing bash/skill consumers;
* deployments still choose the LLM adapter, bash executor, and presentation.
* The plugin intentionally exposes named exports only because Loader default
@@ -18,6 +18,9 @@ import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools
import SkillService, { type Config as SkillRegistryConfig } from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import GoalService, { type Config as GoalDomainConfig } from '@deepseek-ai/dsh-goal'
import * as goalSession from '@deepseek-ai/dsh-goal-session'
import * as toolGoal from '@deepseek-ai/dsh-tool-goal'
import TaskService from '@deepseek-ai/dsh-tasks'
import InvariantService, { type Config as InvariantConfig } from '@deepseek-ai/dsh-invariants'
import * as sessionInvariant from '@deepseek-ai/dsh-session/invariant'
@@ -46,6 +49,14 @@ export interface SkillConfig {
tool?: toolSkill.Config
}
/** Persisted goal domain, model-tool policy, and same-session driver config. */
export interface GoalConfig {
/** Goal-domain creation defaults. */
domain?: GoalDomainConfig
/** Model-facing goal-tool authority policy. */
tool?: toolGoal.Config
}
/**
* Bundle config: each field forwarded verbatim to the child that owns it —
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
@@ -54,9 +65,11 @@ export interface SkillConfig {
* order), the `tools` object to the tool registry (its presentation `mode`),
* `dshHome` to bash environment and local skill discovery, `skills` to the
* skill registry/local provider/tool consumer, `workspaceContext` to the
* workspace-context loader, and `toolBash`/`toolTasks` to the model-facing tool
* plugins this bundle owns. `llmRetry` configures bounded request recovery;
* `invariants` configures global and package-filtered relational checks. Owner schemas supply defaults for optional input;
* workspace-context loader, `llmRetry` to the bounded request-recovery policy,
* and `toolBash`/`toolTasks` to the model-facing tool plugins this bundle owns.
* `goals` opts into and configures the persisted goal domain plus its model tool
* and same-session driver; `invariants` configures global and package-filtered
* relational checks. Owner schemas supply defaults for optional input;
* workspace context instead requires an explicit byte budget or `false` because
* it changes model-visible input. Producer opt-in stays producer-local:
* `toolBash` configures bash only; independently composed producers keep their
@@ -85,6 +98,8 @@ export interface Config {
toolTasks?: toolTasks.Config | false
/** Global enablement and package-name filters for invariant companions. */
invariants?: InvariantConfig
/** Opt-in persisted same-session goal stack; set false or omit to leave it unmounted. */
goals?: GoalConfig | false
/** Bounded transient model-request retry policy. */
llmRetry?: llmRetry.Config
}
@@ -103,6 +118,12 @@ export const ToolBashConfigSchema: z<toolBash.Config> = toolBash.Config
/** The task-control-tool config schema exported for app packages that forward `toolTasks`. */
export const ToolTasksConfigSchema: z<toolTasks.Config> = toolTasks.Config
/** The persisted-goal config schema exported for app packages that opt in. */
export const GoalConfigSchema: z<GoalConfig> = z.object({
domain: GoalService.Config,
tool: toolGoal.Config,
})
/** The bounded LLM retry schema exported for app packages that forward `llmRetry`. */
export const LlmRetryConfigSchema: z<llmRetry.Config> = llmRetry.Config
@@ -118,8 +139,9 @@ export const Config = z.intersect([
toolBash: ToolBashConfigSchema,
toolTasks: z.union([z.const(false), ToolTasksConfigSchema]),
invariants: InvariantService.Config,
goals: z.union([z.const(false), GoalConfigSchema]),
llmRetry: LlmRetryConfigSchema,
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks' | 'invariants' | 'llmRetry'>>,
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks' | 'invariants' | 'goals' | 'llmRetry'>>,
]) as unknown as z<Config>
/**
@@ -139,6 +161,7 @@ export function pickSpineConfig(config: Omit<Config, 'agents'>): Omit<Config, 'a
...config.toolBash !== undefined ? { toolBash: config.toolBash } : {},
...config.toolTasks !== undefined ? { toolTasks: config.toolTasks } : {},
...config.invariants !== undefined ? { invariants: config.invariants } : {},
...config.goals !== undefined ? { goals: config.goals } : {},
...config.llmRetry !== undefined ? { llmRetry: config.llmRetry } : {},
}
}
@@ -177,6 +200,11 @@ export function apply(ctx: Context, config: Config): void {
}
ctx.plugin(AgentRegistry)
ctx.plugin(llmRetry, config.llmRetry ?? {})
if (config.goals !== undefined && config.goals !== false) {
ctx.plugin(GoalService, config.goals.domain ?? {})
ctx.plugin(toolGoal, config.goals.tool ?? {})
ctx.plugin(goalSession)
}
ctx.plugin(TaskService)
ctx.plugin(InvariantService, config.invariants ?? {})
ctx.plugin(sessionInvariant)

View File

@@ -136,6 +136,35 @@ describe('dsh-agent-spine-demo bundle', () => {
expect(ctx.get('tasks')).toBeDefined()
expect(ctx.get('invariants')).toBeDefined()
expect(ctx.get('agentLoop')).toBeDefined()
expect(ctx.get('goals')).toBeUndefined()
await ctx.fiber.dispose()
})
it('opts into the configured persisted-goal domain, tools, and same-session driver', async () => {
const ctx = await mount({
workspaceContext: false,
agents: [{ id: SessionId('configured-goal'), provider: 'mock', model: 'mock' }],
goals: {
domain: { defaultMaxGoalRounds: 17 },
tool: { blockedAfterConsecutiveRounds: 5 },
},
})
const agent = ctx.agents.list()[0]
if (agent === undefined) throw new Error('configured goal test has no live agent')
expect(ctx.goals.create(agent, { objective: 'configured' })).toMatchObject({
objective: 'configured', maxGoalRounds: 17,
})
expect(['create_goal', 'get_goal', 'update_goal'].map(name => ctx.tools.get(name)?.name))
.toEqual(['create_goal', 'get_goal', 'update_goal'])
expect((await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'tool:goal')?.text)
.toContain('at least 5 consecutive goal rounds')
await ctx.fiber.dispose()
})
it('accepts an explicit false goal composition without mounting it', async () => {
const ctx = await mount({ workspaceContext: false, goals: false })
expect(ctx.get('goals')).toBeUndefined()
expect(ctx.tools.get('get_goal')).toBeUndefined()
await ctx.fiber.dispose()
})
@@ -245,6 +274,23 @@ describe('dsh-agent-spine-demo bundle', () => {
await ctx.fiber.dispose()
})
it('uses owner defaults for a schema-bypassing empty goal opt-in', async () => {
const ctx = new Context()
agentCore.apply(ctx, {
workspaceContext: false,
agents: [{ id: SessionId('defaulted-goal'), provider: 'mock', model: 'mock' }],
goals: {},
})
await new Promise(resolve => setTimeout(resolve, 50))
const agent = ctx.agents.list()[0]
if (agent === undefined) throw new Error('default goal test has no live agent')
expect(ctx.goals.create(agent, { objective: 'defaulted' })).toMatchObject({
objective: 'defaulted', maxGoalRounds: 256,
})
expect(ctx.tools.get('get_goal')).toBeDefined()
await ctx.fiber.dispose()
})
it('loads workspace instructions into requests through the bundled spine', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-workspace-context-'))
try {

View File

@@ -41,6 +41,15 @@
{
"path": "../../core/agent"
},
{
"path": "../../goal/goal"
},
{
"path": "../../goal/tool-goal"
},
{
"path": "../../goal/goal-session"
},
{
"path": "../../context/workspace-context"
},

View File

@@ -1,6 +1,6 @@
# @deepseek-ai/dsh-tui-demo
The full-screen terminal app: a Cordis plugin that composes [`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md), JSONL persistence, keyboard-backed user interaction, a pre-created `main` agent, and [`@deepseek-ai/dsh-tui`](../../ui/tui/README.md). Its `bin` boots a leaf `cordis.yml`.
The full-screen terminal app: a Cordis plugin that composes [`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md), persisted same-session goals, the human-command registry and `/goal` producer, JSONL persistence, keyboard-backed user interaction, a pre-created `main` agent, and [`@deepseek-ai/dsh-tui`](../../ui/tui/README.md). Its `bin` boots a leaf `cordis.yml`.
Use [`@deepseek-ai/dsh-cli-demo`](../cli-demo/README.md) for pipes, scripts, and other non-interactive runs. This package requires a TTY pair and has no line-oriented fallback.
@@ -9,6 +9,8 @@ Use [`@deepseek-ai/dsh-cli-demo`](../cli-demo/README.md) for pipes, scripts, and
| Plugin | Why it is here |
|---|---|
| `@deepseek-ai/dsh-agent-spine-demo` | Shared services, model-facing tools, and one configured `main` agent |
| `@deepseek-ai/dsh-commands` | Human-only discovery and dispatch consumed by the TUI and command plugins |
| `@deepseek-ai/dsh-command-goal` | Direct `/goal` status and mutation over the spine's persisted-goal stack |
| `@deepseek-ai/dsh-session-persistence-jsonl` | Durable session log under `persistenceRoot` |
| `@deepseek-ai/dsh-user-interaction` | Provider-neutral human question service |
| `@deepseek-ai/dsh-tui` | Full-screen transcript, editor, tool cards, plan, and question overlays |
@@ -30,6 +32,7 @@ Swappable LLM, bash, filesystem, and other capability providers remain in the le
| `skills` | owner defaults | Skill registry, local provider, and tool config |
| `toolBash` | owner defaults | Model-facing bash tool config |
| `toolTasks` | owner defaults | Background-task control-tool config, or `false` |
| `goals` | owner defaults | Persisted goal-domain and model-tool config; `false` removes the goal stack and `/goal` producer |
| `workspaceContext` | required | Workspace-instruction config, or `false` |
| `persistenceRoot` | `./.sessions` | JSONL persistence root |
| `persistenceCompression` | `'zstd'` | JSONL artifact encoding (`'zstd'` or raw `'none'`) |
@@ -70,7 +73,7 @@ Fresh runs mint a `main-session-<uuid>` session id and pass it to both the TUI a
#### What the model sees
Each non-empty editor submission becomes a user message; a submission during a running turn becomes steering. The shared spine contributes the configured persona, workspace instructions, skill catalog, and visible tool schemas. TUI rendering itself is not model-visible.
Each non-empty non-command editor submission becomes a user message; a submission during a running turn becomes steering. Slash-command input and output remain human-only, while accepted `/goal` mutations append domain-owned model-visible state. The shared spine contributes the configured persona, workspace instructions, skill catalog, goal controls, and visible tool schemas. TUI rendering itself is not model-visible.
#### Token effect

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-tui-demo",
"description": "Full-screen terminal app: agent spine + JSONL persistence + pi-tui front door + pre-created main agent",
"description": "Full-screen terminal app: agent spine + persisted goals + human commands + JSONL persistence + pi-tui front door + pre-created main agent",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -40,6 +40,8 @@
"@deepseek-ai/dsh-app-boot": "^0.0.1",
"@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-agent-spine-demo": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
@@ -59,6 +61,8 @@
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@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-agent-spine-demo": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",

View File

@@ -1,8 +1,8 @@
/**
* Full-screen terminal app: the default agent spine ({@link @deepseek-ai/dsh-agent-spine-demo})
* plus JSONL persistence, keyboard-backed user interaction, and one pre-created
* agent whose exact session identity the TUI drives. Swappable adapters,
* executors, optional tools, and HMR stay in the leaf. This Loader plugin
* plus persisted goals, human commands, JSONL persistence, keyboard-backed
* user interaction, and one pre-created agent whose exact session identity the
* TUI drives. Swappable adapters, executors, optional tools, and HMR stay in the leaf. This Loader plugin
* intentionally exposes named exports only; a default export would hide its
* `Config` schema (see docs/postmortem/0001).
* @module @deepseek-ai/dsh-tui-demo
@@ -13,6 +13,8 @@ import { randomUUID } from 'node:crypto'
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, {
@@ -57,6 +59,8 @@ export interface Config {
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Generic background-task controls forwarded through agent-spine-demo; set false to omit them. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and command. */
goals?: agentCore.GoalConfig | false
/** Persisted session id to resume instead of creating a fresh session. */
resumeSessionId?: string
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
@@ -82,6 +86,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(),
})
@@ -97,6 +102,9 @@ export const Config: z<Config> = z.object({
export function composeTuiApp(ctx: Context, config: Config): void {
const resumeSessionId = config.resumeSessionId === '' ? undefined : config.resumeSessionId
const sessionId = SessionId(resumeSessionId ?? `main-session-${randomUUID()}`)
const goals = config.goals ?? {}
ctx.plugin(CommandService)
if (goals !== false) ctx.plugin(commandGoal)
ctx.plugin(SessionPersistenceJsonl, {
root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT,
...(config.persistenceCompression === undefined ? {} : { compression: config.persistenceCompression }),
@@ -109,6 +117,7 @@ export function composeTuiApp(ctx: Context, config: Config): void {
})
ctx.plugin(agentCore, {
...agentCore.pickSpineConfig(config),
goals,
agents: [{
id: SessionId('main'),
provider: config.provider,

View File

@@ -41,18 +41,22 @@ describe('dsh-tui-demo app', () => {
})
expect(calls.map(call => call.name)).toEqual([
'CommandService',
'command-goal',
'SessionPersistenceJsonl',
'UserInteractionService',
'ui-tui',
'agent-spine-demo',
'tool-ask-user',
])
expect(calls[0]?.config).toEqual({ root: '/tmp/tui-sessions', compression: 'none' })
const tuiConfig = calls[2]?.config as { sessionId: string }
expect(calls[0]?.config).toBeUndefined()
expect(calls[2]?.config).toEqual({ root: '/tmp/tui-sessions', compression: 'none' })
const tuiConfig = calls[4]?.config as { sessionId: string }
expect(tuiConfig).toMatchObject({ welcome: 'TUI ready', color: false, maxToolOutputLines: 3 })
expect(tuiConfig.sessionId).toMatch(/^main-session-[0-9a-f-]{36}$/)
const spineConfig = calls[3]?.config as {
const spineConfig = calls[5]?.config as {
readonly agents: Array<Record<string, unknown>>
readonly goals: Record<string, never>
readonly maxParallelToolCalls: number
readonly persona: string
readonly toolOrder: string[]
@@ -63,6 +67,7 @@ describe('dsh-tui-demo app', () => {
persona: 'test persona',
toolOrder: ['zulu', TOOL_ORDER_REST],
tools: { mode: 'code' },
goals: {},
})
expect(spineConfig.agents[0]).toMatchObject({
id: 'main',
@@ -82,9 +87,9 @@ describe('dsh-tui-demo app', () => {
workspaceContext: false,
})
expect(calls[0]?.config).toEqual({ root: './.sessions' })
expect(calls[2]?.config).toEqual({ welcome: 'ready.', sessionId: 'persisted-session' })
expect((calls[3]?.config as { agents: Array<Record<string, unknown>> }).agents[0]).toMatchObject({
expect(calls[2]?.config).toEqual({ root: './.sessions' })
expect(calls[4]?.config).toEqual({ welcome: 'ready.', sessionId: 'persisted-session' })
expect((calls[5]?.config as { agents: Array<Record<string, unknown>> }).agents[0]).toMatchObject({
id: 'main',
resumeSessionId: 'persisted-session',
})
@@ -96,13 +101,16 @@ describe('dsh-tui-demo app', () => {
provider: 'mock',
model: 'mock-model',
resumeSessionId: '',
goals: false,
workspaceContext: false,
})
const tuiConfig = calls[2]?.config as { sessionId: string }
const tuiConfig = calls[3]?.config as { sessionId: string }
expect(tuiConfig.sessionId).toMatch(/^main-session-[0-9a-f-]{36}$/)
expect((calls[3]?.config as { agents: Array<Record<string, unknown>> }).agents[0])
expect((calls[4]?.config as { agents: Array<Record<string, unknown>> }).agents[0])
.toMatchObject({ sessionId: tuiConfig.sessionId })
expect(calls.map(call => call.name)).not.toContain('command-goal')
expect(calls[4]?.config).toMatchObject({ goals: false })
})
it('has the namespace-plugin export shape so the Loader keeps its schema', () => {

View File

@@ -26,6 +26,12 @@
{
"path": "../../core/session"
},
{
"path": "../../ui/commands"
},
{
"path": "../../goal/command-goal"
},
{
"path": "../agent-spine-demo"
},