From ba693f0355f378b289e536436f774aa914cbd721 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:30:39 +0800 Subject: [PATCH] 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. --- docs/config-catalog.md | 8 ++++ packages/examples/cli-demo/README.md | 4 ++ packages/examples/cli-demo/src/index.ts | 27 +++++++---- .../examples/cli-demo/tests/cli-demo.spec.ts | 48 +++++++++++++++++-- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index fb099937e8..ed8597832a 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -215,16 +215,24 @@ 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 + /** Generic background-task control-tool config forwarded through agent-spine-demo. */ + toolTasks?: NonNullable /** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */ workspaceContext: agentCore.Config['workspaceContext'] } diff --git a/packages/examples/cli-demo/README.md b/packages/examples/cli-demo/README.md index 5197e3bcd0..c938f6c583 100644 --- a/packages/examples/cli-demo/README.md +++ b/packages/examples/cli-demo/README.md @@ -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 | diff --git a/packages/examples/cli-demo/src/index.ts b/packages/examples/cli-demo/src/index.ts index e58bcdad06..1308209681 100644 --- a/packages/examples/cli-demo/src/index.ts +++ b/packages/examples/cli-demo/src/index.ts @@ -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 + /** Generic background-task control-tool config forwarded through agent-spine-demo. */ + toolTasks?: NonNullable /** 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 = 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 = 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 }) } diff --git a/packages/examples/cli-demo/tests/cli-demo.spec.ts b/packages/examples/cli-demo/tests/cli-demo.spec.ts index 52433abe81..343d6184d7 100644 --- a/packages/examples/cli-demo/tests/cli-demo.spec.ts +++ b/packages/examples/cli-demo/tests/cli-demo.spec.ts @@ -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 { +async function mount(config: cliDemo.Config, withBash = false): Promise { 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 }).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()