Merge remote-tracking branch 'origin/master' into codex/cli-one-shot-demo

# Conflicts:
#	AGENTS.md
#	docs/architecture.md
#	docs/capability-seams.md
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	knip.json
#	packages/README.md
#	packages/support/loader-smoke/README.md
#	packages/support/loader-smoke/src/index.ts
#	vitest.snapshot.config.ts
This commit is contained in:
Tianyi Cui
2026-07-19 13:11:50 +08:00
851 changed files with 46937 additions and 13076 deletions

View File

@@ -8,12 +8,14 @@ The package mounts no console logger, readline UI, user-interaction service, or
| Key | Default | Routed to |
|---|---|---|
| `provider` | required | the pre-created `main` agent's provider route |
| `model` | required | the pre-created `main` agent's model |
| `persona` | — | the deployment persona in `dsh-system-prompt` |
| `toolOrder` | lexicographic | explicit model-facing tool order in `dsh-system-prompt` |
| `tools` | `{ mode: 'native' }` | tool-registry presentation config through `dsh-agent-spine-demo` |
| `skills` | owner defaults | skill registry, local provider, and model-facing skill tool |
| `persistenceRoot` | `./.sessions` | JSONL session root |
| `workspaceContext` | required | workspace-instruction byte budget, or `false` to disable loading |
Each process creates a new session whose workspace cwd is the launch directory. The app has no resume setting.
@@ -51,7 +53,7 @@ The headless-agent leaf supplies local bash, filesystem, skill, subagent, workfl
### One-shot task turn
**What the model sees**: The positional task becomes one user message. Through `dsh-agent-spine-demo`, the `main` agent also receives the configured persona, skill catalog, visible tool schemas, and retained tool results needed for later steps in the same turn.
**What the model sees**: The positional task becomes one user message. Through `dsh-agent-spine-demo`, the `main` agent also receives configured workspace instructions and persona, the skill catalog, visible tool schemas, and retained tool results needed for later steps in the same turn.
**Token effect**: The task, prompt sections, tool schemas, assistant output, and tool results consume tokens on each model step. JSON event streaming and final rendering add no model tokens; delegated child work has its own model usage and is not included in the parent result's `usage` total.

View File

@@ -39,6 +39,7 @@
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"@deepseek-ai/dsh-workspace-context": "^0.0.1",
"cordis": "^4.0.0-rc.7",
"schemastery": "^3.17.0"
},
@@ -53,6 +54,7 @@
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-workspace-context": "workspace:^",
"cordis": "^4.0.0-rc.7",
"schemastery": "^3.17.0"
}

View File

@@ -6,7 +6,7 @@
import { parseArgs } from 'node:util'
import type { Context } from 'cordis'
import { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { TokenUsage } from '@deepseek-ai/dsh-llm'
import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'
import { boot, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
@@ -40,7 +40,7 @@ export interface CliResult {
readonly usage?: TokenUsage
}
/** Options for one turn against the pre-created `main` agent. */
/** Options for one turn against the configured top-level agent. */
export interface OneShotOptions {
/** Exactly one nonblank user task. */
readonly task: string
@@ -186,17 +186,20 @@ async function waitForStartupIdle(agent: Agent, signal?: AbortSignal): Promise<v
}
/**
* Run one message-triggered turn on the pre-created `main` agent, aggregate its
* Run one message-triggered turn on the configured top-level agent, aggregate its
* final text and model usage, wait for idle plus an explicit persistence flush,
* and return its durable ending. Only the exact main-session task turn reaches
* and return its durable ending. Only the selected agent's task turn reaches
* `onEvent`; startup injections and unrelated sessions are ignored.
* @param ctx - settled Loader root containing `ctx.agents` and `ctx.sessions`.
* @param options - task, optional cancellation, and optional stream observer.
* @returns the DSH-native result envelope after durable quiescence.
*/
export async function runOneShot(ctx: Context, options: OneShotOptions): Promise<CliResult> {
const agent = ctx.get('agents')?.get(AgentId('main'))
if (agent === undefined) throw new Error('config did not create the required "main" agent')
const agents = ctx.get('agents')?.roots() ?? []
const [agent] = agents
if (agent === undefined || agents.length !== 1) {
throw new Error(`config must create exactly one top-level agent, found ${agents.length}`)
}
await waitForStartupIdle(agent, options.signal)
let targetTurn: number | undefined

View File

@@ -8,10 +8,11 @@
import type { Context } from 'cordis'
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'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
const DEFAULT_PERSISTENCE_ROOT = './.sessions'
@@ -19,6 +20,8 @@ export const name = 'cli-demo'
/** App config forwarded to the spine, pre-created agent, and JSONL backend. */
export interface Config {
/** Provider route for the `main` agent. */
provider: string
/** Model name for the `main` agent; a matching adapter must be registered. */
model: string
/** Deployment persona forwarded to the system-prompt plugin. */
@@ -31,9 +34,12 @@ export interface Config {
persistenceRoot?: string
/** Skill registry, local-provider, and model-facing consumer config. */
skills?: agentCore.SkillConfig
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
}
export const Config: z<Config> = z.object({
provider: z.string().required(),
model: z.string().required(),
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
persona: z.string(),
@@ -41,6 +47,7 @@ export const Config: z<Config> = z.object({
// Absent means lexicographic order; schemastery's native array default is [].
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
tools: ToolRegistry.Config,
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
})
/**
@@ -52,7 +59,8 @@ export const Config: z<Config> = z.object({
*/
export function apply(ctx: Context, config: Config): void {
const spineConfig: agentCore.Config = {
agents: [{ id: AgentId('main'), model: config.model, cwd: process.cwd() }],
agents: [{ id: SessionId('main'), provider: config.provider, model: config.model, cwd: process.cwd() }],
workspaceContext: config.workspaceContext,
}
if (config.persona !== undefined) spineConfig.persona = config.persona
if (config.toolOrder !== undefined) spineConfig.toolOrder = config.toolOrder

View File

@@ -13,6 +13,7 @@ const dshPackages = [
'core/system-prompt', 'core/tools', 'core/agent-loop', 'llm/llm', 'bash/bash',
'bash/bash-local', 'bash/tool-bash', 'support/invariants', 'ui/app-boot',
'session-persistence/session-persistence', 'session-persistence/session-persistence-jsonl',
'context/workspace-context',
]
const vendorPackages = ['cordis', 'loader', 'include', 'timer', 'schemastery', 'cosmokit']
@@ -67,9 +68,11 @@ async function makeConsumer(): Promise<string> {
'- id: cli-agent',
" name: '@deepseek-ai/dsh-cli-demo'",
' config:',
' provider: built-cli-mock',
' model: built-cli-mock',
" persona: 'built CLI test'",
" persistenceRoot: './.sessions'",
' workspaceContext: false',
'',
].join('\n'))
return dir

View File

@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { AgentId, agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import type { Message } from '@deepseek-ai/dsh-llm'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import { afterEach, describe, expect, it } from 'vitest'
@@ -44,13 +44,15 @@ describe('dsh-cli-demo app composition', () => {
it('composes the UI-less spine, JSONL persistence, and a main agent', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cli-demo-compose-'))
const ctx = await mount({
provider: 'mock',
model: 'mock',
persona: 'Headless.',
tools: { mode: 'native' },
persistenceRoot: root,
skills: await skillConfig(),
workspaceContext: false,
})
const agent = ctx.get('agents')?.get(AgentId('main'))
const [agent] = ctx.get('agents')?.roots() ?? []
expect(ctx.get('agentLoop')).toBeDefined()
expect(ctx.get('sessionPersistence')).toBeDefined()
expect(agent?.session.header.cwd).toBe(process.cwd())
@@ -67,10 +69,11 @@ describe('dsh-cli-demo app composition', () => {
try {
const ctx = new Context()
contexts.push(ctx)
cliDemo.apply(ctx, { model: 'mock' })
cliDemo.apply(ctx, { provider: 'mock', model: 'mock', workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 80))
expect(ctx.get('sessionPersistence')).toBeDefined()
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
const [agent] = ctx.get('agents')?.roots() ?? []
expect(agent?.session.id).toMatch(/^main-session-/)
expect(await ctx.skills.list()).toEqual([])
} finally {
if (oldDshHome === undefined) delete process.env.DSH_HOME
@@ -80,9 +83,11 @@ describe('dsh-cli-demo app composition', () => {
}
const ctx = await mount({
provider: 'mock',
model: 'mock',
toolOrder: ['zulu', TOOL_ORDER_REST],
skills: await skillConfig(6),
workspaceContext: false,
})
ctx.skills.register({ name: 'cli-skill', description: 'CLI skill', source: 'runtime', content: 'body' })
for (const name of ['alpha', 'zulu']) {

View File

@@ -2,7 +2,7 @@ import { readdir, mkdtemp } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { Context } from 'cordis'
import { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { CallId, LlmAdapter, type GenerateOptions, type StreamChunk, type TokenUsage } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
import { afterEach, describe, expect, it } from 'vitest'
@@ -93,9 +93,11 @@ async function harness(script: readonly ScriptEntry[]): Promise<Harness> {
const ctx = new Context()
liveContexts.push(ctx)
await ctx.plugin(cliDemo, {
provider: 'mock',
model: 'mock',
persistenceRoot: root,
skills: { local: { dshHome: join(skillHome, '.dsh'), agentsHome: join(skillHome, '.agents') } },
workspaceContext: false,
})
await new Promise(resolve => setTimeout(resolve, 80))
ctx.llm.registerAdapter(['mock'], new ScriptedAdapter(script))
@@ -105,7 +107,7 @@ async function harness(script: readonly ScriptEntry[]): Promise<Harness> {
parameters: { text: { type: 'string', required: true } },
execute: async args => [{ type: 'text', text: `ECHO: ${(args as { text: string }).text}` }],
})
const agent = ctx.agents.get(AgentId('main'))
const [agent] = ctx.agents.roots()
if (agent === undefined) throw new Error('test main agent missing')
return { ctx, agent, persistenceRoot: root }
}
@@ -304,7 +306,7 @@ describe('runOneShot and executeCli', () => {
const empty = new Context()
liveContexts.push(empty)
await expect(runOneShot(empty, { task: 'task' })).rejects.toThrow('required "main" agent')
await expect(runOneShot(empty, { task: 'task' })).rejects.toThrow('exactly one top-level agent')
const final = await harness([textResponse('answer')])
const output = await invoke(final.ctx, ['task'], { failStdout: true })