feat(ui): configure maxParallelToolCalls for factory-created agents
The acp and stdio-agent plugins only forwarded `model` into their created agents, so every factory/ACP deployment was pinned to the agent-loop default parallel cap with no cordis.yml override. Add a `maxParallelToolCalls` config field (positive-integer validated) to both, threaded through the existing per-agent options path — symmetric with `model`.
This commit is contained in:
@@ -18,6 +18,12 @@ Requires: `agents` · `sessions` · `sessionPersistence` · `tools` · `userInte
|
|||||||
export interface AcpConfig {
|
export interface AcpConfig {
|
||||||
/** Model name for created agents (must have a registered adapter). */
|
/** Model name for created agents (must have a registered adapter). */
|
||||||
model?: string
|
model?: string
|
||||||
|
/**
|
||||||
|
* Maximum tool calls each created agent runs concurrently within one assistant
|
||||||
|
* step (a positive integer; the agent loop defaults it when omitted). `1`
|
||||||
|
* preserves fully serial execution.
|
||||||
|
*/
|
||||||
|
maxParallelToolCalls?: number
|
||||||
/**
|
/**
|
||||||
* Transport stream override. Production omits this (the plugin wires
|
* Transport stream override. Production omits this (the plugin wires
|
||||||
* `process.stdin`/`process.stdout` via `ndJsonStream`). Tests inject an
|
* `process.stdin`/`process.stdout` via `ndJsonStream`). Tests inject an
|
||||||
@@ -591,6 +597,12 @@ Source: [`packages/skill/skill-local/src/index.ts:39`](../packages/skill/skill-l
|
|||||||
export interface Config {
|
export interface Config {
|
||||||
/** Model name for the `main` agent (must have a registered adapter). */
|
/** Model name for the `main` agent (must have a registered adapter). */
|
||||||
model: string
|
model: string
|
||||||
|
/**
|
||||||
|
* Maximum tool calls the `main` agent runs concurrently within one assistant
|
||||||
|
* step (a positive integer; the agent loop defaults it when omitted). `1`
|
||||||
|
* preserves fully serial execution.
|
||||||
|
*/
|
||||||
|
maxParallelToolCalls?: number
|
||||||
/** Deployment persona (the system-prompt plugin's `persona` config). */
|
/** Deployment persona (the system-prompt plugin's `persona` config). */
|
||||||
persona?: string
|
persona?: string
|
||||||
/** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
|
/** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ It is a **client-driver / UI plugin**, the structured analogue of the readline `
|
|||||||
| Key | Default | Meaning |
|
| Key | Default | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `model` | — | Model name for created agents (must have a registered adapter). |
|
| `model` | — | Model name for created agents (must have a registered adapter). |
|
||||||
|
| `maxParallelToolCalls` | (agent-loop default) | Positive integer cap on tool calls each created agent runs concurrently within one assistant step; `1` is fully serial. |
|
||||||
|
|
||||||
(No persona key: `dsh-system-prompt`'s own `persona` config supplies the global default section, so ACP-created agents render it without the bridge carrying prompt text. An agent-scoped same-name section may still shadow that default.)
|
(No persona key: `dsh-system-prompt`'s own `persona` config supplies the global default section, so ACP-created agents render it without the bridge carrying prompt text. An agent-scoped same-name section may still shadow that default.)
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,12 @@ function stringArrayContent(
|
|||||||
export interface AcpConfig {
|
export interface AcpConfig {
|
||||||
/** Model name for created agents (must have a registered adapter). */
|
/** Model name for created agents (must have a registered adapter). */
|
||||||
model?: string
|
model?: string
|
||||||
|
/**
|
||||||
|
* Maximum tool calls each created agent runs concurrently within one assistant
|
||||||
|
* step (a positive integer; the agent loop defaults it when omitted). `1`
|
||||||
|
* preserves fully serial execution.
|
||||||
|
*/
|
||||||
|
maxParallelToolCalls?: number
|
||||||
/**
|
/**
|
||||||
* Transport stream override. Production omits this (the plugin wires
|
* Transport stream override. Production omits this (the plugin wires
|
||||||
* `process.stdin`/`process.stdout` via `ndJsonStream`). Tests inject an
|
* `process.stdin`/`process.stdout` via `ndJsonStream`). Tests inject an
|
||||||
@@ -260,6 +266,9 @@ export interface AcpConfig {
|
|||||||
|
|
||||||
export const Config: Schema<AcpConfig> = Schema.object({
|
export const Config: Schema<AcpConfig> = Schema.object({
|
||||||
model: Schema.string(),
|
model: Schema.string(),
|
||||||
|
// A positive integer; a bad value (0, negative, fractional) fails config
|
||||||
|
// validation here rather than being silently dropped from cordis.yml.
|
||||||
|
maxParallelToolCalls: Schema.number().step(1).min(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1010,12 +1019,13 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
|||||||
* Build per-agent options from the plugin config, omitting absent fields
|
* Build per-agent options from the plugin config, omitting absent fields
|
||||||
* (exactOptionalPropertyTypes: never assign `undefined` to an optional key).
|
* (exactOptionalPropertyTypes: never assign `undefined` to an optional key).
|
||||||
* Exported for unit coverage of both the present and absent branches.
|
* Exported for unit coverage of both the present and absent branches.
|
||||||
* @param config - the plugin config carrying the optional model name.
|
* @param config - the plugin config carrying the optional model name and parallel cap.
|
||||||
* @returns the per-agent options, with `model` present only when configured.
|
* @returns the per-agent options, with each field present only when configured.
|
||||||
*/
|
*/
|
||||||
export function agentOptions(config: AcpConfig): { model?: string } {
|
export function agentOptions(config: AcpConfig): { model?: string; maxParallelToolCalls?: number } {
|
||||||
return {
|
return {
|
||||||
...config.model !== undefined ? { model: config.model } : {},
|
...config.model !== undefined ? { model: config.model } : {},
|
||||||
|
...config.maxParallelToolCalls !== undefined ? { maxParallelToolCalls: config.maxParallelToolCalls } : {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -818,5 +818,7 @@ describe('agentOptions', () => {
|
|||||||
it('includes only the fields present in config', () => {
|
it('includes only the fields present in config', () => {
|
||||||
expect(agentOptions({})).toEqual({})
|
expect(agentOptions({})).toEqual({})
|
||||||
expect(agentOptions({ model: 'm' })).toEqual({ model: 'm' })
|
expect(agentOptions({ model: 'm' })).toEqual({ model: 'm' })
|
||||||
|
expect(agentOptions({ maxParallelToolCalls: 3 })).toEqual({ maxParallelToolCalls: 3 })
|
||||||
|
expect(agentOptions({ model: 'm', maxParallelToolCalls: 1 })).toEqual({ model: 'm', maxParallelToolCalls: 1 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
|
|||||||
| Key | Default | Routed to |
|
| Key | Default | Routed to |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `model` | (required) | the pre-created `main` agent's model |
|
| `model` | (required) | the pre-created `main` agent's model |
|
||||||
|
| `maxParallelToolCalls` | (agent-loop default) | positive integer cap on tool calls the `main` agent runs concurrently within one assistant step (`1` is fully serial), routed to `dsh-agent-loop` |
|
||||||
| `persona` | — | the deployment persona template (may reference `{{model}}`), routed to `dsh-system-prompt` |
|
| `persona` | — | the deployment persona template (may reference `{{model}}`), 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` |
|
| `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` |
|
||||||
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ export const name = 'stdio-agent'
|
|||||||
export interface Config {
|
export interface Config {
|
||||||
/** Model name for the `main` agent (must have a registered adapter). */
|
/** Model name for the `main` agent (must have a registered adapter). */
|
||||||
model: string
|
model: string
|
||||||
|
/**
|
||||||
|
* Maximum tool calls the `main` agent runs concurrently within one assistant
|
||||||
|
* step (a positive integer; the agent loop defaults it when omitted). `1`
|
||||||
|
* preserves fully serial execution.
|
||||||
|
*/
|
||||||
|
maxParallelToolCalls?: number
|
||||||
/** Deployment persona (the system-prompt plugin's `persona` config). */
|
/** Deployment persona (the system-prompt plugin's `persona` config). */
|
||||||
persona?: string
|
persona?: string
|
||||||
/** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
|
/** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
|
||||||
@@ -87,6 +93,9 @@ export interface Config {
|
|||||||
|
|
||||||
export const Config: z<Config> = z.object({
|
export const Config: z<Config> = z.object({
|
||||||
model: 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.
|
||||||
|
maxParallelToolCalls: z.number().step(1).min(1),
|
||||||
persona: z.string(),
|
persona: z.string(),
|
||||||
// The array default is forced to undefined: ABSENT means "lexicographic
|
// The array default is forced to undefined: ABSENT means "lexicographic
|
||||||
// order" (the owning dsh-system-prompt schema does the same), while
|
// order" (the owning dsh-system-prompt schema does the same), while
|
||||||
@@ -116,6 +125,7 @@ export function apply(ctx: Context, config: Config): void {
|
|||||||
id: AgentId('main'),
|
id: AgentId('main'),
|
||||||
model: config.model,
|
model: config.model,
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
|
...config.maxParallelToolCalls !== undefined ? { maxParallelToolCalls: config.maxParallelToolCalls } : {},
|
||||||
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
|
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
|
||||||
}],
|
}],
|
||||||
...config.skills !== undefined ? { skills: config.skills } : {},
|
...config.skills !== undefined ? { skills: config.skills } : {},
|
||||||
|
|||||||
@@ -136,6 +136,17 @@ describe('dsh-stdio-agent app', () => {
|
|||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('forwards maxParallelToolCalls onto the pre-created agent when set', async () => {
|
||||||
|
const ctx = await mount({
|
||||||
|
model: 'mock',
|
||||||
|
maxParallelToolCalls: 3,
|
||||||
|
persistenceRoot: '/tmp/dsh-stdio-agent-spec-parallel',
|
||||||
|
skills: await isolatedSkillsConfig(),
|
||||||
|
})
|
||||||
|
expect(ctx.get('agents')?.get(AgentId('main'))?.options.maxParallelToolCalls).toBe(3)
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
it('exposes its name and Config schema', () => {
|
it('exposes its name and Config schema', () => {
|
||||||
expect(stdioAgent.name).toBe('stdio-agent')
|
expect(stdioAgent.name).toBe('stdio-agent')
|
||||||
expect(stdioAgent.Config).toBeDefined()
|
expect(stdioAgent.Config).toBeDefined()
|
||||||
|
|||||||
Reference in New Issue
Block a user