Merge latest origin/master into parallel-tool-call
This commit is contained in:
@@ -25,9 +25,10 @@ Because the package wires no logger entry, an ACP leaf has **nothing to get wron
|
||||
|
||||
| Key | Default | Routed to |
|
||||
|---|---|---|
|
||||
| `model` | (required) | the per-session agent template the bridge creates agents from |
|
||||
| `provider` | (required) | the initial provider route for each per-session agent the bridge creates; ACP model selection may replace it per session |
|
||||
| `model` | (required) | the initial model for each per-session agent; ACP clients may switch among adapter-advertised models |
|
||||
| `maxParallelToolCalls` | agent-loop default | positive-integer concurrent tool-call cap shared by the bundled loop's agents; `1` is serial |
|
||||
| `persona` | — | the deployment persona template (may reference `{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` |
|
||||
| `persona` | — | the deployment persona template (may reference `{{provider}}`/`{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` |
|
||||
| `toolOrder` | — | explicit model-facing tool order (a name list with one `'<unlisted-tools>'` rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to `dsh-system-prompt` |
|
||||
| `dshHome` | `$DSH_HOME` or `~/.dsh` | Harness home exposed to model bash and used by local skill discovery |
|
||||
| `tools` | `{ mode: 'native' }` | tool-registry presentation config (`native` / `code` / `both`), routed through `dsh-agent-spine-demo` |
|
||||
|
||||
@@ -21,7 +21,7 @@ import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
export const name = 'acp-demo'
|
||||
|
||||
/**
|
||||
* App config: the swappable per-deployment values. `model` configures the
|
||||
* App config: the swappable per-deployment values. `provider` and `model` configure the
|
||||
* agent template the ACP bridge creates each session's agent from (NOT a
|
||||
* pre-created agent — ACP creates agents at `session/new`); `persona` is the
|
||||
* deployment persona (forwarded to the system-prompt plugin); `toolOrder` is
|
||||
@@ -31,6 +31,8 @@ export const name = 'acp-demo'
|
||||
* agent loop; `persistenceRoot` is the JSONL backend's directory.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Provider route for ACP-created agents. */
|
||||
provider: string
|
||||
/** Model name for ACP-created agents (must have a registered adapter). */
|
||||
model: string
|
||||
/**
|
||||
@@ -62,6 +64,7 @@ export interface Config {
|
||||
// the common fields would make two small app contracts depend on a new facade.
|
||||
/* jscpd:ignore-start */
|
||||
export const Config: z<Config> = z.object({
|
||||
provider: z.string().required(),
|
||||
model: z.string().required(),
|
||||
// A positive integer; a bad value (0, negative, fractional) fails config
|
||||
// validation here rather than being silently dropped from cordis.yml.
|
||||
@@ -88,11 +91,11 @@ export const Config: z<Config> = z.object({
|
||||
* NO agents (its `agents` list defaults to `[]`) and carries the deployment
|
||||
* `persona`; the JSONL backend persists under `persistenceRoot`; the ACP
|
||||
* bridge owns stdout for JSON-RPC and creates one agent per `session/new`
|
||||
* from `model`. No logger, no `hmr` — stdout stays pure.
|
||||
* from the provider/model pair. No logger, no `hmr` — stdout stays pure.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.plugin(agentCore, agentCore.pickSpineConfig(config))
|
||||
ctx.plugin(UserInteractionService)
|
||||
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
|
||||
ctx.plugin(acp, { model: config.model })
|
||||
ctx.plugin(acp, { provider: config.provider, model: config.model })
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
|
||||
|
||||
describe('dsh-acp-demo composition', () => {
|
||||
it('brings up the spine + persistence + the ACP bridge', async () => {
|
||||
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-acp-demo-test', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
const ctx = await mount({ provider: 'mock', model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-acp-demo-test', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
expect(ctx.get('agents')).toBeDefined()
|
||||
expect(ctx.get('sessions')).toBeDefined()
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
@@ -89,7 +89,7 @@ describe('dsh-acp-demo composition', () => {
|
||||
// persistenceRoot, so the runtime fallback is the one that fires.
|
||||
const ctx = new Context()
|
||||
// No persona: covers the omitted-persona forwarding branch too.
|
||||
acpAgent.apply(ctx, { model: 'mock', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
acpAgent.apply(ctx, { provider: 'mock', model: 'mock', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
await ctx.fiber.dispose()
|
||||
@@ -97,6 +97,7 @@ describe('dsh-acp-demo composition', () => {
|
||||
|
||||
it('forwards explicit project-instruction controls to the bundled spine', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
persona: 'hi',
|
||||
persistenceRoot: '/tmp/dsh-acp-demo-workspace-context',
|
||||
@@ -110,7 +111,7 @@ describe('dsh-acp-demo composition', () => {
|
||||
it('uses default skill config when apply is called directly without skills', async () => {
|
||||
await withIsolatedSkillHomes(async () => {
|
||||
const ctx = new Context()
|
||||
acpAgent.apply(ctx, { model: 'mock', workspaceContext: false })
|
||||
acpAgent.apply(ctx, { provider: 'mock', model: 'mock', workspaceContext: false })
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
expect(ctx.skills).toBeDefined()
|
||||
expect(await ctx.skills.list()).toEqual([])
|
||||
@@ -120,7 +121,7 @@ describe('dsh-acp-demo composition', () => {
|
||||
|
||||
it('forwards skill config and dshHome into agent-spine-demo', async () => {
|
||||
const skills = await isolatedSkillsConfig(6)
|
||||
const ctx = await mount({ model: 'mock', persona: 'hi', dshHome: skills.local!.dshHome!, skills, workspaceContext: false })
|
||||
const ctx = await mount({ provider: 'mock', model: 'mock', persona: 'hi', dshHome: skills.local!.dshHome!, skills, workspaceContext: false })
|
||||
ctx.skills.register({ name: 'acp-skill', description: 'ACP skill', source: 'runtime', content: 'body' })
|
||||
expect(JSON.stringify(await composePrefix(ctx))).toContain('- `acp-skill`: ACP...')
|
||||
await ctx.fiber.dispose()
|
||||
@@ -128,6 +129,7 @@ describe('dsh-acp-demo composition', () => {
|
||||
|
||||
it('forwards maxParallelToolCalls to the bundled agent loop', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
maxParallelToolCalls: 3,
|
||||
persistenceRoot: '/tmp/dsh-acp-demo-test-parallel',
|
||||
@@ -140,6 +142,7 @@ describe('dsh-acp-demo composition', () => {
|
||||
|
||||
it('forwards bundled tool config into agent-core', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
workspaceContext: false,
|
||||
toolBash: { enableRunInBackground: false },
|
||||
@@ -159,6 +162,7 @@ describe('dsh-acp-demo composition', () => {
|
||||
|
||||
it('forwards toolOrder through agent-spine-demo to the system-prompt assembly', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
toolOrder: ['zulu', TOOL_ORDER_REST],
|
||||
persistenceRoot: '/tmp/dsh-acp-demo-test-tool-order',
|
||||
|
||||
@@ -78,12 +78,12 @@ async function makeConsumer(): Promise<string> {
|
||||
' name: \'@deepseek-ai/dsh-llm-deepseek\'',
|
||||
' config:',
|
||||
' apiKey: !!js process.env.DEEPSEEK_API_KEY',
|
||||
' models: [deepseek-v4-flash]',
|
||||
'- id: bash',
|
||||
' name: \'@deepseek-ai/dsh-bash-local\'',
|
||||
'- id: acp-agent',
|
||||
' name: \'@deepseek-ai/dsh-acp-demo\'',
|
||||
' config:',
|
||||
' provider: deepseek',
|
||||
' model: deepseek-v4-flash',
|
||||
' persona: \'test agent\'',
|
||||
' workspaceContext: false',
|
||||
|
||||
@@ -36,12 +36,12 @@ const CORDIS_YML = `
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
models: [deepseek-v4-flash]
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
- id: acp-agent
|
||||
name: '@deepseek-ai/dsh-acp-demo'
|
||||
config:
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persona: 'You are a test agent.'
|
||||
workspaceContext: false
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
|
||||
it('forwards a pre-created agent to the loop and the persona to system-prompt', async () => {
|
||||
const ctx = await mount({
|
||||
agents: [{ id: AgentId('main'), model: 'mock' }],
|
||||
agents: [{ id: AgentId('main'), provider: 'mock', model: 'mock' }],
|
||||
persona: 'You are main.',
|
||||
workspaceContext: false,
|
||||
})
|
||||
@@ -147,7 +147,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
|
||||
it('forwards the global maxParallelToolCalls config to agent-loop', async () => {
|
||||
const ctx = await mount({
|
||||
agents: [{ id: AgentId('main'), model: 'mock' }],
|
||||
agents: [{ id: AgentId('main'), provider: 'mock', model: 'mock' }],
|
||||
maxParallelToolCalls: 3,
|
||||
workspaceContext: false,
|
||||
})
|
||||
@@ -181,7 +181,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
agentId: AgentId('main'),
|
||||
sessionId: SessionId('main-session'),
|
||||
meta: { cwd: root },
|
||||
agentOptions: { model: 'mock' },
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
const agent = handle.agent
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
agentId: AgentId('main'),
|
||||
sessionId: SessionId('main-disabled-session'),
|
||||
meta: { cwd: root },
|
||||
agentOptions: { model: 'mock' },
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
|
||||
handle.agent.send([{ type: 'text', text: 'hi' }])
|
||||
@@ -302,7 +302,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
agentId: AgentId('main'),
|
||||
sessionId: SessionId('prefix-order-session'),
|
||||
meta: { cwd: root },
|
||||
agentOptions: { model: 'mock' },
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
|
||||
handle.agent.send([{ type: 'text', text: 'hi' }])
|
||||
|
||||
@@ -11,7 +11,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
|
||||
| Plugin | Why it is here |
|
||||
|---|---|
|
||||
| `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) |
|
||||
| `@deepseek-ai/dsh-agent-spine-demo` | the spine, pre-creating a `main` agent from this app's `model` with `process.cwd()` as the fresh session cwd and carrying its `persona` |
|
||||
| `@deepseek-ai/dsh-agent-spine-demo` | the spine, pre-creating a `main` agent from this app's provider/model pair with `process.cwd()` as the fresh session cwd and carrying its `persona` |
|
||||
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
|
||||
| `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool |
|
||||
@@ -25,9 +25,10 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
|
||||
|
||||
| Key | Default | Routed to |
|
||||
|---|---|---|
|
||||
| `provider` | (required) | the pre-created `main` agent's registered provider route |
|
||||
| `model` | (required) | the pre-created `main` agent's model |
|
||||
| `maxParallelToolCalls` | agent-loop default | positive-integer concurrent tool-call cap shared by the bundled loop's agents; `1` is serial |
|
||||
| `persona` | — | the deployment persona template (may reference `{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` |
|
||||
| `persona` | — | the deployment persona template (may reference `{{provider}}`/`{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` |
|
||||
| `toolOrder` | — | explicit model-facing tool order (a name list with one `'<unlisted-tools>'` rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to `dsh-system-prompt` |
|
||||
| `dshHome` | `$DSH_HOME` or `~/.dsh` | Harness home exposed to model bash and used by local skill discovery |
|
||||
| `tools` | `{ mode: 'native' }` | tool-registry presentation config (`native` / `code` / `both`), routed through `dsh-agent-spine-demo` |
|
||||
@@ -56,7 +57,6 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
models: [deepseek-v4-flash]
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
@@ -64,6 +64,7 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persona: 'You are a coding assistant powered by the {{model}} model.'
|
||||
```
|
||||
|
||||
@@ -26,7 +26,7 @@ export const name = 'stdio-demo'
|
||||
|
||||
/**
|
||||
* App config: the swappable per-demo values, each routed to where the app wires
|
||||
* it. `model`/`resumeSessionId` configure the pre-created `main` agent (through
|
||||
* it. `provider`/`model`/`resumeSessionId` configure the pre-created `main` agent (through
|
||||
* {@link @deepseek-ai/dsh-agent-spine-demo}'s forwarded `agents` list); `persona` is
|
||||
* the deployment persona (forwarded to the system-prompt plugin); `toolOrder`
|
||||
* is the explicit model-facing tool order (forwarded to the system-prompt plugin);
|
||||
@@ -35,6 +35,8 @@ export const name = 'stdio-demo'
|
||||
* `welcome` is the UI banner.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Provider route for the `main` agent. */
|
||||
provider: string
|
||||
/** Model name for the `main` agent (must have a registered adapter). */
|
||||
model: string
|
||||
/**
|
||||
@@ -71,6 +73,7 @@ export interface Config {
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
provider: z.string().required(),
|
||||
model: z.string().required(),
|
||||
// A positive integer; a bad value (0, negative, fractional) fails config
|
||||
// validation here rather than being silently dropped from cordis.yml.
|
||||
@@ -106,6 +109,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
...agentCore.pickSpineConfig(config),
|
||||
agents: [{
|
||||
id: AgentId('main'),
|
||||
provider: config.provider,
|
||||
model: config.model,
|
||||
cwd: process.cwd(),
|
||||
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
|
||||
|
||||
@@ -90,6 +90,7 @@ async function makeConsumer(
|
||||
'- id: stdio-agent',
|
||||
' name: \'@deepseek-ai/dsh-stdio-demo\'',
|
||||
' config:',
|
||||
' provider: mock',
|
||||
' model: mock-echo',
|
||||
' persona: \'demo\'',
|
||||
' workspaceContext: false',
|
||||
|
||||
@@ -66,7 +66,7 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
|
||||
|
||||
describe('dsh-stdio-demo app', () => {
|
||||
it('composes the spine + front-door cluster and pre-creates the main agent', async () => {
|
||||
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-demo-spec', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
const ctx = await mount({ provider: 'mock', model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-demo-spec', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
// The spine services (brought up by the agent-spine-demo bundle) are all present.
|
||||
expect(ctx.get('agents')).toBeDefined()
|
||||
expect(ctx.get('agentLoop')).toBeDefined()
|
||||
@@ -87,7 +87,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
// schema-bypassing direct-mount caller.
|
||||
const ctx = new Context()
|
||||
// No persona: covers the omitted-persona forwarding branch too.
|
||||
stdioAgent.apply(ctx, { model: 'mock', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
stdioAgent.apply(ctx, { provider: 'mock', model: 'mock', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||
@@ -96,6 +96,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
|
||||
it('forwards explicit project-instruction controls to the bundled spine', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
persona: 'hi',
|
||||
persistenceRoot: '/tmp/dsh-stdio-demo-spec-workspace-context',
|
||||
@@ -108,7 +109,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
it('uses default skill config when apply is called directly without skills', async () => {
|
||||
await withIsolatedSkillHomes(async () => {
|
||||
const ctx = new Context()
|
||||
stdioAgent.apply(ctx, { model: 'mock', workspaceContext: false })
|
||||
stdioAgent.apply(ctx, { provider: 'mock', model: 'mock', workspaceContext: false })
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
expect(ctx.skills).toBeDefined()
|
||||
expect(await ctx.skills.list()).toEqual([])
|
||||
@@ -121,6 +122,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
// session the resume is contained + logged, so no `main` agent registers —
|
||||
// the branch that maps resumeSessionId through is what this covers.
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
persona: 'hi',
|
||||
persistenceRoot: '/tmp/dsh-stdio-demo-spec-resume',
|
||||
@@ -134,7 +136,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
|
||||
it('forwards skill config and dshHome into agent-spine-demo', async () => {
|
||||
const skills = await isolatedSkillsConfig(6)
|
||||
const ctx = await mount({ model: 'mock', persona: 'hi', dshHome: skills.local!.dshHome!, skills, workspaceContext: false })
|
||||
const ctx = await mount({ provider: 'mock', model: 'mock', persona: 'hi', dshHome: skills.local!.dshHome!, skills, workspaceContext: false })
|
||||
ctx.skills.register({ name: 'stdio-skill', description: 'Stdio skill', source: 'runtime', content: 'body' })
|
||||
expect(JSON.stringify(await composePrefix(ctx))).toContain('- `stdio-skill`: Std...')
|
||||
await ctx.fiber.dispose()
|
||||
@@ -142,6 +144,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
|
||||
it('forwards maxParallelToolCalls to the bundled agent loop', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
maxParallelToolCalls: 3,
|
||||
persistenceRoot: '/tmp/dsh-stdio-demo-spec-parallel',
|
||||
@@ -154,6 +157,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
|
||||
it('forwards bundled tool config into agent-core', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
workspaceContext: false,
|
||||
toolBash: { enableRunInBackground: false },
|
||||
@@ -173,6 +177,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
|
||||
it('forwards toolOrder through agent-spine-demo to the system-prompt assembly', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
toolOrder: ['zulu', TOOL_ORDER_REST],
|
||||
persistenceRoot: '/tmp/dsh-stdio-demo-spec-tool-order',
|
||||
|
||||
Reference in New Issue
Block a user