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()