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:
Dudu-0223
2026-07-14 11:55:39 +08:00
parent 8c8e5fdd24
commit eae8b8ce2e
7 changed files with 50 additions and 3 deletions

View File

@@ -26,6 +26,7 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
| Key | Default | Routed to |
|---|---|---|
| `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` |
| `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 |

View File

@@ -65,6 +65,12 @@ export const name = 'stdio-agent'
export interface Config {
/** Model name for the `main` agent (must have a registered adapter). */
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). */
persona?: string
/** 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({
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(),
// The array default is forced to undefined: ABSENT means "lexicographic
// 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'),
model: config.model,
cwd: process.cwd(),
...config.maxParallelToolCalls !== undefined ? { maxParallelToolCalls: config.maxParallelToolCalls } : {},
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
}],
...config.skills !== undefined ? { skills: config.skills } : {},

View File

@@ -136,6 +136,17 @@ describe('dsh-stdio-agent app', () => {
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', () => {
expect(stdioAgent.name).toBe('stdio-agent')
expect(stdioAgent.Config).toBeDefined()