feat(todo): make the parallel in_progress policy configurable

Whether concurrent active tasks are legitimate depends on runtime
concurrency the tool cannot observe, but whether a deployment's agents
ever fan out is knowable at composition time. `allowParallelInProgress`
(default true) therefore replaces the hardcoded policy: the flag moves
the model-facing instruction and the accepted input together, so a
deployment running strictly sequential agents can restore the
single-active discipline from cordis.yml.

The durable-log invariant does not follow the flag. A log written while
parallel work was allowed must still replay after a deployment tightens
the policy, so the invariant stays silent on the active count.
This commit is contained in:
Chinesezjc
2026-07-29 14:49:45 +08:00
parent 39d65df019
commit 3a61c4c568
13 changed files with 339 additions and 45 deletions
@@ -26,11 +26,11 @@ function agentWithSession(id = 'parent-1'): Agent & { session: Session } {
return { id: SessionId(id), session } as unknown as Agent & { session: Session }
}
async function setup(): Promise<Context> {
async function setup(config: tool.Config = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(tool)
await ctx.plugin(tool, config)
return ctx
}
@@ -140,6 +140,62 @@ describe('dsh-tool-todo', () => {
expect(agent.session.events.findLast(e => e.type === 'todo/write')!.data.todos).toEqual(todos)
})
describe('allowParallelInProgress: false', () => {
const parallel = [
{ content: 'run subagent a', status: 'in_progress' },
{ content: 'run subagent b', status: 'in_progress' },
]
it('rejects a call marking several items in_progress', async () => {
const ctx = await setup({ allowParallelInProgress: false })
const agent = agentWithSession('single-active')
const result = await callTodo(ctx, { todos: parallel }, { agent })
expect(result.isError).toBe(true)
expect(text(result)).toContain('at most one task may be in_progress')
// A rejected call must not reach the durable log.
expect(agent.session.events.some(e => e.type === 'todo/write')).toBe(false)
})
it('still accepts one active item', async () => {
const ctx = await setup({ allowParallelInProgress: false })
const todos: TodoItem[] = [
{ content: 'run subagent a', status: 'in_progress' },
{ content: 'run subagent b', status: 'pending' },
]
const result = await callTodo(ctx, { todos })
expect(result.isError).toBe(false)
})
it('an explicit true accepts a parallel write, like the omitted default', async () => {
const ctx = await setup({ allowParallelInProgress: true })
const result = await callTodo(ctx, { todos: parallel })
expect(result.isError).toBe(false)
})
it('defaults to parallel for a direct apply, which bypasses the schema default', async () => {
// Composing through ctx.plugin lets schemastery fill the field; a caller
// invoking apply() itself hands over a config object with it absent, so
// the policy default has to hold on that path too.
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
tool.apply(ctx, {})
const result = await callTodo(ctx, { todos: parallel })
expect(result.isError).toBe(false)
})
it('instructs the model to keep at most one active, and the default instructs parallel', async () => {
const single = await setup({ allowParallelInProgress: false })
const singleDesc = single.tools.schemas().find(s => s.name === 'todo_write')!.description
expect(singleDesc).toContain('Keep AT MOST ONE todo `in_progress`')
expect(singleDesc).not.toContain('several at once')
const parallelDesc = (await setup()).tools.schemas().find(s => s.name === 'todo_write')!.description
expect(parallelDesc).toContain('several at once when work genuinely runs in parallel')
expect(parallelDesc).not.toContain('AT MOST ONE')
})
})
it.each([
{ label: 'empty content', todos: [{ content: ' ', status: 'pending' }], fragment: 'non-empty' },
{ label: 'duplicate content', todos: [{ content: 'dup', status: 'pending' }, { content: 'dup', status: 'completed' }], fragment: 'duplicate' },