Merge latest origin/master into worktree/agent-execution-context-rfc

# Conflicts:
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	packages/examples/README.md
This commit is contained in:
Tianyi Cui
2026-07-19 13:04:29 +08:00
167 changed files with 8686 additions and 991 deletions

View File

@@ -4,12 +4,12 @@ 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 + agent-execution + invariants + `tool-bash` + workspace-context + `tool-skill` + `agent-loop`) |
| `stdio-demo/` | `@deepseek-ai/dsh-stdio-demo` | Terminal stdio chat app: the spine + console logger + readline UI + 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 (`timer` + `llm` + sessions + system-prompt + tools + skills + agents + invariants + `tool-bash` + workspace-context + `tool-skill` + `agent-loop`) |
| `stdio-demo/` | `@deepseek-ai/dsh-stdio-demo` | Terminal chat app: the spine + JSONL persistence + TTY-selected `dsh-tui`/`dsh-stdio` front door + a pre-created `main` agent, with a boot `bin` |
| `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` |
| `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; `stdio-demo` and `acp-demo` compose it with opposite front-door clusters (console logger + readline UI vs the stdout-owning ACP bridge) 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.
`agent-spine-demo` is the shared bundle; `stdio-demo` and `acp-demo` compose it with terminal and ACP front-door clusters 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.
These are **not** product API. The spine pieces they bundle live in [`core/`](../core/README.md), the bridges/channels/boot-glue in [`ui/`](../ui/README.md), and the swappable backends (LLM adapter, bash executor) in their capability groups; a demo bundle just picks one concrete composition of them. Swap or fork one freely.

View File

@@ -35,7 +35,7 @@ The spine is everything COMMON to every front door. The swappable and front-door
- **the LLM adapter** — the bundle ships the abstract `llm` service; the leaf registers a concrete adapter on `ctx.llm` (`llm-deepseek`, `llm-pi-ai`, `llm-replay`).
- **the bash executor** — the bundle ships `tool-bash` (the consumer schema); the leaf provides `ctx.bash` (`bash-local` or a sandboxed impl).
- **non-local skill providers** — the bundle ships the skill registry, the local filesystem provider, and the `skill` tool; deployments can add other providers such as embedded or remote catalogs as siblings.
- **presentation + per-app infra** — the stdio UI / ACP bridge, a console logger, `hmr`. These form the coupled "front-door cluster" that the app packages ([`dsh-stdio-demo`](../../examples/stdio-demo/README.md), [`dsh-acp-demo`](../../examples/acp-demo/README.md)) bake in. `timer` is in the spine (common to both, stdout-silent); a console logger is NOT (it writes to stdout, which the ACP bridge reserves for JSON-RPC).
- **presentation + per-app infra** — the terminal (`dsh-tui` / `dsh-stdio`) or ACP front door and `hmr`. These form the coupled front-door cluster that the app packages ([`dsh-stdio-demo`](../../examples/stdio-demo/README.md), [`dsh-acp-demo`](../../examples/acp-demo/README.md)) bake in. `timer` is in the spine because it is common and stdout-silent; front doors own stdout and remain outside.
This is the [interface/implementation/consumer seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md) raised to the composition level: the bundle owns the shared spine, the leaf owns the backends, the app package owns the front door.

View File

@@ -1,8 +1,8 @@
# @deepseek-ai/dsh-stdio-demo
The **terminal stdio chat app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-spine-demo`](../../examples/agent-spine-demo/README.md)) with the front-door cluster a terminal chat needs, and a `bin` that boots a leaf `cordis.yml`.
The **terminal chat app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-spine-demo`](../../examples/agent-spine-demo/README.md)) with JSONL persistence, human interaction, a pre-created `main` agent, and a TTY-selected pi-tui/readline front door. Its `bin` boots a leaf `cordis.yml`.
It is the readline counterpart to [`@deepseek-ai/dsh-acp-demo`](../acp-demo/README.md): both consume the same spine, but each bakes in the OPPOSITE front-door cluster.
It is the terminal counterpart to [`@deepseek-ai/dsh-acp-demo`](../acp-demo/README.md): both consume the same spine, while ACP reserves stdout for JSON-RPC and creates sessions from the client.
## What it bakes in
@@ -10,12 +10,13 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
| Plugin | Why it is here |
|---|---|
| `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) |
| `@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-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 |
| `@deepseek-ai/dsh-stdio` | the readline UI, bound to the exact app-owned agent/session identity and rendering it as `main` |
| `@cordisjs/plugin-logger-console` | readline diagnostics for non-TTY operation; omitted from the fullscreen TUI path |
| `@deepseek-ai/dsh-stdio` | the line-oriented channel for pipes and automation, bound to the exact app-owned agent/session identity |
| `@deepseek-ai/dsh-tui` | the fullscreen interactive channel for TTY pairs, bound to the same exact identity |
`@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, NOT baked in here: it is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader`, and the in-process test tier cannot even import it (so a package whose `apply` statically pulled it in could never carry the per-file coverage gate). Unlike the console logger, a stray `hmr` is not a stdout-purity footgun, so leaving it at the leaf costs no safety. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`.
@@ -36,10 +37,11 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
| `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` |
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
| `welcome` | `ready.` | the stdin-chat banner |
| `welcome` | `ready.` | terminal banner / TUI subtitle |
| `ui` | `{ mode: 'auto' }` | terminal mode (`auto` / `readline` / `tui`) and nested TUI presentation config |
| `resumeSessionId` | — | resume a persisted session id instead of starting fresh (sourced from an env var in the leaf) |
Fresh stdio sessions use the process launch directory as `session.header.cwd` and mint one combined `main-session-<uuid>` agent/session id, so durable restarts cannot collide. The app passes that exact opaque id to both its config-created agent and UI; an AgentLoop-only reload resumes materialized history under that id, while the UI's `main` text remains only a display label and never selects another registry root by prefix or insertion order. Readline buffers nonblank startup input for that identity until `agent/session-start`, so piped stdin cannot outrun asynchronous exact-id restoration or let EOF discard the queued prompt; `agent-loop/config-start-failed` instead drains and reports buffered input so a missing or corrupt persisted session cannot hang EOF. A resumed run binds both components to the exact `resumeSessionId` and keeps the cwd stored in the persisted session header.
Fresh terminal sessions use the process launch directory as `session.header.cwd` and mint one combined `main-session-<uuid>` agent/session id, so durable restarts cannot collide. The app passes that exact opaque id to the config-created agent and selected UI before agent-core starts; this lets either front door observe `agent-loop/config-start-failed`, and an AgentLoop-only reload restores materialized history under the same id. Readline buffers startup input until `agent/session-start`; the TUI waits to enter fullscreen until the matching root appears. A resumed run binds both components to the exact `resumeSessionId` and keeps the persisted cwd.
## The bin
@@ -67,6 +69,8 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd` an
provider: deepseek
model: deepseek-v4-flash
persona: 'You are a coding assistant powered by the {{model}} model.'
ui:
mode: auto
```
Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — "swap the backend, keep the app".
@@ -75,9 +79,9 @@ Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo —
### Composed terminal agent request
**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 readline submission becomes a user message.
**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 terminal submission becomes a user message; submissions made while the agent runs steer the active turn.
**Token effect**: Child prompt and schema costs repeat per request; user input and tool history grow until compaction. The welcome banner, logger output, and rendered transcript are terminal-only and add zero model tokens.
**Token effect**: Child prompt and schema costs repeat per request; user input and tool history grow until compaction. Terminal banners, logger output, cards, and rendered transcripts add zero model tokens.
### Human-answer result
@@ -87,6 +91,6 @@ Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo —
## Known Limitations and Deferred Work
- **One pre-created `main` agent drives the readline UI** — there is no multi-session or concurrent-agent surface in this app; a run is one conversation.
- **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.
- **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

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-stdio-demo",
"description": "Terminal stdio chat app: the agent-spine-demo bundle + console logger + readline UI + a pre-created main agent, with a bin to boot a leaf cordis.yml",
"description": "Terminal chat app: agent spine + JSONL persistence + TTY pi-tui/readline front-door selection + pre-created main agent",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -32,8 +32,8 @@
"peerDependencies": {
"@cordisjs/plugin-include": "^1.0.4",
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-app-boot": "^0.0.1",
"@cordisjs/plugin-logger-console": "^1.0.0",
"@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-llm": "^0.0.1",
@@ -42,6 +42,7 @@
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
"@deepseek-ai/dsh-stdio": "^0.0.1",
"@deepseek-ai/dsh-tui": "^0.0.1",
"@deepseek-ai/dsh-tool-ask-user": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
@@ -51,8 +52,8 @@
"devDependencies": {
"@cordisjs/plugin-include": "workspace:^",
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@cordisjs/plugin-logger-console": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
@@ -62,6 +63,7 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-stdio": "workspace:^",
"@deepseek-ai/dsh-tui": "workspace:^",
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-interaction": "workspace:^",

View File

@@ -2,7 +2,7 @@
/**
* Boot a stdio app from a leaf `cordis.yml`; usage is `dsh-stdio-demo [config]`, defaulting to the
* cwd file. Shared `.env` loading, fail-loud Loader guards, and settled-tree boot live in
* dsh-app-boot. The echo and REPL demos invoke this bin with their own leaf configs.
* dsh-app-boot. The echo and coding-agent demos invoke this bin with their own leaf configs.
* @module @deepseek-ai/dsh-stdio-demo/bin
*/

View File

@@ -1,9 +1,9 @@
/**
* The stdio chat app: the default agent spine ({@link @deepseek-ai/dsh-agent-spine-demo}) plus the
* coupled front-door cluster a terminal chat needs — a console logger, the independently
* packaged readline UI, JSONL session persistence, the user-interaction seam with its
* coupled front-door cluster a terminal chat needs — TTY-selected pi-tui/readline
* presentation, JSONL session persistence, the user-interaction seam with its
* `ask_user_question` tool, and one pre-created agent whose exact shared
* agent/session identity the UI drives under its `main` display label.
* agent/session identity the selected UI drives under its `main` display label.
* 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).
@@ -22,11 +22,46 @@ import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
import * as uiStdio from '@deepseek-ai/dsh-stdio'
import * as uiTui from '@deepseek-ai/dsh-tui'
export const name = 'stdio-demo'
const DEFAULT_PERSISTENCE_ROOT = './.sessions'
const DEFAULT_WELCOME = 'ready.'
/** Terminal front door selected by the app bundle. */
export type TerminalMode = 'auto' | 'readline' | 'tui'
/** App-level terminal selection with nested TUI presentation settings. */
export interface UiConfig {
/** Select a concrete front door or infer it from the process streams. */
mode?: TerminalMode
/** Settings forwarded only when the pi-tui front door is selected. */
tui?: uiTui.TuiConfig
}
const terminalModeSchema = z.union(['auto', 'readline', 'tui'] as const).default('auto')
/** Schemastery schema for app-level terminal selection. */
export const UiConfigSchema: z<UiConfig> = z.object({
mode: terminalModeSchema,
tui: uiTui.TuiConfigSchema,
})
/**
* Resolve the app's terminal front door.
* @param config - app-level terminal selection.
* @param isTTY - whether both process streams are interactive TTYs.
* @returns the concrete UI package to mount.
*/
export function resolveTerminalMode(config: UiConfig | undefined, isTTY: boolean): Exclude<TerminalMode, 'auto'> {
const mode = config?.mode ?? 'auto'
if (mode === 'auto') return isTTY ? 'tui' : 'readline'
if (mode === 'tui' && !isTTY) {
throw new Error('stdio-demo: TUI mode requires both stdin and stdout to be TTYs; use mode "readline" for pipes')
}
return mode
}
/**
* App config: the swappable per-demo values, each routed to where the app wires
* it. `provider`/`model`/`resumeSessionId` configure the pre-created `main` agent (through
@@ -35,7 +70,7 @@ const DEFAULT_WELCOME = 'ready.'
* is the explicit model-facing tool order (forwarded to the system-prompt plugin);
* fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions
* keep their persisted cwd. `persistenceRoot` is the JSONL backend's directory;
* `welcome` is the UI banner.
* `welcome` is the UI banner and `ui` configures terminal mode/presentation.
*/
export interface Config {
/** Provider route for the `main` agent. */
@@ -56,6 +91,8 @@ export interface Config {
persistenceRoot?: string
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
welcome?: string
/** Terminal front-door selection and pi-tui presentation settings. */
ui?: UiConfig
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
@@ -85,6 +122,7 @@ export const Config: z<Config> = z.object({
dshHome: z.string(),
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
welcome: z.string().default(DEFAULT_WELCOME),
ui: UiConfigSchema,
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
toolTasks: agentCore.ToolTasksConfigSchema,
@@ -93,23 +131,34 @@ export const Config: z<Config> = z.object({
})
/**
* Compose the spine with the stdio front door. Console logging, persistence,
* and user interaction mount first; the readline UI then waits on the agent
* registry and subscribes to config-start failures before agent-core can start
* the configured identity. The ask-user tool waits on the completed spine.
* The `hmr` dev-reload plugin is a leaf concern (see the module doc), so it is
* not mounted here.
* Compose the spine with one terminal front door. Persistence and user
* interaction mount first; the selected UI then waits on the exact session id
* and subscribes to config-start failures before agent-core starts it. Console
* logging is readline-only because fullscreen output belongs to pi-tui. The
* ask-user tool waits on the completed spine, and HMR remains a leaf concern.
* @param ctx - context receiving the app's child plugins.
* @param config - app configuration routed to the spine and front door.
* @param isTTY - whether both process streams are interactive TTYs.
*/
export function apply(ctx: Context, config: Config): void {
export function composeTerminalApp(ctx: Context, config: Config, isTTY: boolean): void {
const resumeSessionId = config.resumeSessionId === '' ? undefined : config.resumeSessionId
const sessionId = SessionId(resumeSessionId ?? `main-session-${randomUUID()}`)
ctx.plugin(ConsoleExporter)
const mode = resolveTerminalMode(config.ui, isTTY)
if (mode === 'readline') ctx.plugin(ConsoleExporter)
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT })
ctx.plugin(UserInteractionService)
ctx.plugin(uiStdio, {
welcome: config.welcome ?? DEFAULT_WELCOME,
sessionId,
})
if (mode === 'tui') {
ctx.plugin(uiTui, {
...config.ui?.tui,
welcome: config.welcome ?? DEFAULT_WELCOME,
sessionId,
})
} else {
ctx.plugin(uiStdio, {
welcome: config.welcome ?? DEFAULT_WELCOME,
sessionId,
})
}
ctx.plugin(agentCore, {
...agentCore.pickSpineConfig(config),
agents: [{
@@ -122,3 +171,11 @@ export function apply(ctx: Context, config: Config): void {
})
ctx.plugin(toolAskUser)
}
/** Compose the configured terminal front door with the agent app. */
/* v8 ignore start -- production stream capability wiring; composeTerminalApp is unit-covered,
and the coding-agent PTY smoke covers the interactive process path */
export function apply(ctx: Context, config: Config): void {
composeTerminalApp(ctx, config, process.stdin.isTTY && process.stdout.isTTY)
}
/* v8 ignore stop */

View File

@@ -11,8 +11,8 @@ import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import * as stdioAgent from '../src/index.ts'
/**
* Unit coverage for app composition and config forwarding: console logger, pre-created main agent,
* agent-spine-demo spine, JSONL backend, and readline UI. HMR is a Loader-only leaf concern covered by the
* Unit coverage for app composition and config forwarding: pre-created main agent,
* agent-spine-demo spine, JSONL backend, and adaptive terminal UI. HMR is a Loader-only leaf concern covered by the
* keyless echo smoke; this tier pins the export shape because an inject-less app could otherwise
* survive namespace collapse while silently losing its schema.
*/
@@ -66,6 +66,63 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
}
describe('dsh-stdio-demo app', () => {
it('selects readline for pipes and dsh-tui for interactive terminal pairs', () => {
expect(stdioAgent.resolveTerminalMode(undefined, false)).toBe('readline')
expect(stdioAgent.resolveTerminalMode(undefined, true)).toBe('tui')
expect(stdioAgent.resolveTerminalMode({ mode: 'readline' }, true)).toBe('readline')
expect(stdioAgent.resolveTerminalMode({ mode: 'tui' }, true)).toBe('tui')
expect(() => stdioAgent.resolveTerminalMode({ mode: 'tui' }, false)).toThrow('requires both stdin and stdout')
})
it('binds only the selected terminal package to the app-owned exact session identity', () => {
const calls: Array<{ name: string; config: unknown }> = []
const ctx = {
plugin(plugin: { name?: string }, config?: unknown) {
calls.push({ name: plugin.name ?? '', config })
},
} as unknown as Context
stdioAgent.composeTerminalApp(ctx, {
provider: 'mock',
model: 'mock',
workspaceContext: false,
welcome: 'TUI ready',
ui: { mode: 'tui', tui: { color: false, maxToolOutputLines: 3 } },
}, true)
expect(calls.map(call => call.name)).toContain('ui-tui')
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 }
expect(tuiConfig).toMatchObject({ welcome: 'TUI ready', color: false, maxToolOutputLines: 3 })
expect(tuiConfig.sessionId).toMatch(/^main-session-/)
const spineConfig = calls.find(call => call.name === 'agent-spine-demo')?.config as {
agents: Array<{ id: string; sessionId?: string; resumeSessionId?: string }>
}
expect(spineConfig.agents[0]).toMatchObject({ id: 'main', sessionId: tuiConfig.sessionId })
calls.length = 0
stdioAgent.composeTerminalApp(ctx, {
provider: 'mock',
model: 'mock',
resumeSessionId: 'persisted-session',
workspaceContext: false,
ui: { mode: 'tui' },
}, true)
expect(calls.find(call => call.name === 'ui-tui')?.config).toMatchObject({
sessionId: 'persisted-session', welcome: 'ready.',
})
expect((calls.find(call => call.name === 'agent-spine-demo')?.config as typeof spineConfig).agents[0])
.toMatchObject({ id: 'main', resumeSessionId: 'persisted-session' })
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')
})
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 })
// The spine services (brought up by the agent-spine-demo bundle) are all present.

View File

@@ -41,6 +41,9 @@
{
"path": "../../ui/stdio"
},
{
"path": "../../ui/tui"
},
{
"path": "../../ui/tool-ask-user"
},