Merge origin/master into feat/tui-package

Master unifies every live agent with its exact SessionId and moves declarative startup failures to agent-loop/config-start-failed. Keeping the branch’s AgentId label binding would let the TUI target the wrong lifecycle after reload and would miss asynchronous resume failures.

Resolve that contract migration by giving the selected terminal front door the same generated or resumed SessionId as agent-core, mounting the front door first, and entering fullscreen only after the matching root appears. Refresh the source-derived catalogs and keyless terminal goldens so Code Mode, workflow, Cordis-tool, and transient UI scenarios all exercise the merged identity model.
This commit is contained in:
Tianyi Cui
2026-07-19 11:37:14 +08:00
280 changed files with 4311 additions and 4148 deletions
+43 -39
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 — the independently packaged
* pi-tui and readline front doors, JSONL session persistence, the user-interaction
* seam with its `ask_user_question` tool, and a pre-created `main` agent the UI
* drives. Interactive terminals use `dsh-tui`; pipes use `dsh-stdio` plus logging.
* 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 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).
@@ -11,9 +11,9 @@
*/
import type { Context } from 'cordis'
import { randomUUID } from 'node:crypto'
import ConsoleExporter from '@cordisjs/plugin-logger-console'
import z from 'schemastery'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo'
@@ -25,6 +25,8 @@ 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'
@@ -39,7 +41,7 @@ export interface UiConfig {
const terminalModeSchema = z.union(['auto', 'readline', 'tui'] as const).default('auto')
/** Schemastery schema for the app-level terminal selection. */
/** Schemastery schema for app-level terminal selection. */
export const UiConfigSchema: z<UiConfig> = z.object({
mode: terminalModeSchema,
tui: uiTui.TuiConfigSchema,
@@ -47,10 +49,9 @@ export const UiConfigSchema: z<UiConfig> = z.object({
/**
* 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.
* @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'
@@ -88,7 +89,7 @@ export interface Config {
dshHome?: string
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** Terminal banner printed once on start. Defaults to `'ready.'`. */
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
welcome?: string
/** Terminal front-door selection and pi-tui presentation settings. */
ui?: UiConfig
@@ -99,7 +100,7 @@ export interface Config {
/** Generic background-task control-tool config forwarded through agent-core. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/**
* If set, the `main` agent RESUMES this persisted session id instead of
* 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`
* (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`).
*/
@@ -119,10 +120,8 @@ export const Config: z<Config> = z.object({
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
tools: ToolRegistry.Config,
dshHome: z.string(),
// TODO(single-default-literal): share these schema defaults and defensive
// apply() fallbacks through named constants while retaining both boundaries.
persistenceRoot: z.string().default('./.sessions'),
welcome: z.string().default('ready.'),
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
welcome: z.string().default(DEFAULT_WELCOME),
ui: UiConfigSchema,
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
@@ -132,40 +131,45 @@ export const Config: z<Config> = z.object({
})
/**
* Compose the spine with one terminal front door. Interactive TTY pairs mount
* `dsh-tui` without a console exporter; pipes mount the readline `dsh-stdio`
* channel with the console logger. The `hmr` dev-reload plugin 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 terminal streams are interactive TTYs.
* 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 composeTerminalApp(ctx: Context, config: Config, isTTY: boolean): void {
const resumeSessionId = config.resumeSessionId === '' ? undefined : config.resumeSessionId
const sessionId = SessionId(resumeSessionId ?? `main-session-${randomUUID()}`)
const mode = resolveTerminalMode(config.ui, isTTY)
if (mode === 'readline') ctx.plugin(ConsoleExporter)
ctx.plugin(agentCore, {
...agentCore.pickSpineConfig(config),
agents: [{
id: AgentId('main'),
provider: config.provider,
model: config.model,
cwd: process.cwd(),
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
}],
})
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT })
ctx.plugin(UserInteractionService)
ctx.plugin(toolAskUser)
if (mode === 'tui') {
ctx.plugin(uiTui, {
...config.ui?.tui,
welcome: config.welcome ?? 'ready.',
agent: 'main',
welcome: config.welcome ?? DEFAULT_WELCOME,
sessionId,
})
} else {
ctx.plugin(uiStdio, { welcome: config.welcome ?? 'ready.', agent: 'main' })
ctx.plugin(uiStdio, {
welcome: config.welcome ?? DEFAULT_WELCOME,
sessionId,
})
}
ctx.plugin(agentCore, {
...agentCore.pickSpineConfig(config),
agents: [{
id: SessionId('main'),
provider: config.provider,
model: config.model,
cwd: process.cwd(),
...resumeSessionId === undefined ? { sessionId } : { resumeSessionId: sessionId },
}],
})
ctx.plugin(toolAskUser)
}
/** Compose the configured terminal front door with the agent app. */