fix(cli-demo): forward the complete spine config

Align the one-shot app with the shared agent-spine contract that landed on master after this branch began. Expose maxParallelToolCalls, dshHome, toolBash, and toolTasks through the Loader schema and route them with pickSpineConfig().

This restores deployment control over tool-call concurrency, the shared Harness home, background bash, and task_output wait bounds instead of silently retaining owner defaults.

Exercise all four fields through the composed runtime, document the package-level contract, and regenerate the config catalog from the owning interface.
This commit is contained in:
Tianyi Cui
2026-07-19 14:30:39 +08:00
parent ae419fb692
commit ba693f0355
4 changed files with 76 additions and 11 deletions

View File

@@ -10,10 +10,14 @@ The package mounts no console logger, readline UI, user-interaction service, or
|---|---|---|
| `provider` | required | the configured agent's provider route |
| `model` | required | the configured agent's model |
| `maxParallelToolCalls` | agent-loop default | positive-integer concurrent tool-call cap; `1` is serial |
| `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` |
| `dshHome` | `$DSH_HOME` or `~/.dsh` | Harness home exposed to model bash and used by local skill discovery |
| `skills` | owner defaults | skill registry, local provider, and model-facing skill tool |
| `toolBash` | owner defaults | model-facing bash config, including this producer's background opt-in |
| `toolTasks` | owner defaults | generic `task_output` wait bounds |
| `persistenceRoot` | `./.sessions` | JSONL session root |
| `workspaceContext` | required | workspace-instruction byte budget, or `false` to disable loading |

View File

@@ -24,31 +24,47 @@ export interface Config {
provider: string
/** Model name for the configured agent; a matching adapter must be registered. */
model: string
/** Bundled agent-loop concurrency cap; `1` is serial and omission uses its default. */
maxParallelToolCalls?: number
/** Deployment persona forwarded to the system-prompt plugin. */
persona?: string
/** Explicit model-facing tool order forwarded to the system-prompt plugin. */
toolOrder?: string[]
/** Tool-registry presentation config forwarded through agent-spine-demo. */
tools?: ToolsConfig
/** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
dshHome?: string
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** Skill registry, local-provider, and model-facing consumer config. */
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-spine-demo. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Generic background-task control-tool config forwarded through agent-spine-demo. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
}
// Each front door keeps a complete Loader schema so its deployment contract is
// readable without a cross-package config facade.
/* jscpd:ignore-start */
export const Config: z<Config> = z.object({
provider: z.string().required(),
model: z.string().required(),
maxParallelToolCalls: z.number().step(1).min(1),
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
persona: z.string(),
dshHome: z.string(),
skills: agentCore.SkillConfigSchema,
// Absent means lexicographic order; schemastery's native array default is [].
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
tools: ToolRegistry.Config,
toolBash: agentCore.ToolBashConfigSchema,
toolTasks: agentCore.ToolTasksConfigSchema,
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
})
/* jscpd:ignore-end */
/**
* Compose the UI-less spine, a fresh top-level agent rooted at the process cwd,
@@ -58,14 +74,9 @@ export const Config: z<Config> = z.object({
* @param config - validated app configuration.
*/
export function apply(ctx: Context, config: Config): void {
const spineConfig: agentCore.Config = {
ctx.plugin(agentCore, {
...agentCore.pickSpineConfig(config),
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
if (config.tools !== undefined) spineConfig.tools = config.tools
if (config.skills !== undefined) spineConfig.skills = config.skills
ctx.plugin(agentCore, spineConfig)
})
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT })
}

View File

@@ -4,9 +4,10 @@ import { join } from 'node:path'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import type { Message } from '@deepseek-ai/dsh-llm'
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import { afterEach, describe, expect, it } from 'vitest'
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as cliDemo from '../src/index.ts'
const contexts: Context[] = []
@@ -19,8 +20,9 @@ async function skillConfig(catalogDescriptionMaxLength?: number): Promise<NonNul
}
}
async function mount(config: cliDemo.Config): Promise<Context> {
async function mount(config: cliDemo.Config, withBash = false): Promise<Context> {
const ctx = new Context()
if (withBash) ctx.provide('bash', { sandboxMode: undefined })
contexts.push(ctx)
await ctx.plugin(cliDemo, config)
await new Promise(resolve => setTimeout(resolve, 80))
@@ -104,6 +106,46 @@ describe('dsh-cli-demo app composition', () => {
])
})
it('forwards the complete shared spine configuration', async () => {
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-cli-demo-home-'))
const agentsHome = await mkdtemp(join(tmpdir(), 'dsh-cli-demo-agents-'))
const ctx = await mount({
provider: 'mock',
model: 'mock',
maxParallelToolCalls: 3,
dshHome,
skills: { local: { agentsHome } },
toolBash: { enableRunInBackground: false },
toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 },
workspaceContext: false,
}, true)
expect(ctx.get('agentLoop')?.config.maxParallelToolCalls).toBe(3)
const execution: ToolExecution = {
token: Symbol('cli-demo-dsh-home-test') as ToolExecution['token'],
callId: CallId('cli-demo-dsh-home'),
name: 'bash',
arguments: { command: 'true' },
}
expect(ctx.bashEnv.collect(execution)).toMatchObject({ DSH_HOME: dshHome })
const bash = ctx.tools.schemas().find(tool => tool.name === 'bash')
expect(Object.keys((bash!.parameters as { properties: Record<string, unknown> }).properties))
.not.toContain('run_in_background')
const id = ctx.tasks.start({
kind: 'bash',
label: 'config forwarding probe',
run: () => ({ cancel: () => {}, done: Promise.resolve({ status: 'completed' }) }),
})
const wait = vi.spyOn(ctx.tasks, 'wait')
await ctx.tools.execute({
callId: CallId('cli-demo-task-config'),
name: 'task_output',
arguments: { task_id: id, wait: true },
})
expect(wait).toHaveBeenCalledWith(id, 7, undefined, undefined)
})
it('exposes the Loader-safe namespace plugin shape and schema', () => {
expect(cliDemo.name).toBe('cli-demo')
expect(cliDemo.Config).toBeDefined()