fix(agent-loop): tighten parallel tool-call safety

This commit is contained in:
Dudu-0223
2026-07-16 15:52:35 +08:00
parent bbf66b3a5b
commit 17fe9e5b1e
26 changed files with 88 additions and 67 deletions

View File

@@ -35,12 +35,17 @@ sequenceDiagram
Driver->>Hooks: <code>agent/step-result</code> waterfall
Driver->>Session: <code>assistant/message</code>
Driver->>Tools: group calls by executionMode
loop started tool calls (bounded pool)
Driver->>Session: <code>tool/call</code> pending audit
Driver->>Tools: ordered pre / pooled dispatch / ordered post
Tools-->>Session: tool-owned events when applicable
loop bounded rolling pool until group drains
opt capacity available for an unstarted call
Driver->>Session: <code>tool/call</code> pending audit
Driver->>Tools: ordered pre / pooled dispatch
Tools-->>Session: tool-owned events when applicable
end
opt next model-order result is ready
Driver->>Tools: ordered post
Driver->>Session: <code>tool/result</code>
end
end
Driver->>Session: <code>tool/result</code> in model order
Driver->>Session: <code>step/end</code>
Driver->>Hooks: <code>agent/turn-continuation</code> waterfall
Driver->>Hooks: <code>agent/turn-stop</code> serial terminal checkpoint

View File

@@ -84,10 +84,9 @@ forever:
'assistant/message'
schedule tool calls by ctx.tools.executionMode (exclusive = barrier;
consecutive parallel-safe = one rolling-pool group, <= maxParallelToolCalls in flight):
each started call:
'tool/call'
tools/pre-execute -> monotonic guards -> tools/execute -> tools/post-execute -> tools/result
'tool/result' committed in model order (slot-buffered)
while the bounded pool has work:
capacity available -> 'tool/call' -> tools/pre-execute -> monotonic guards -> tools/execute
next model-order slot ready -> tools/post-execute -> 'tool/result'
append post-tool context (model order) and steering
'step/end'
agent/turn-continuation

View File

@@ -37,11 +37,17 @@ Source: [`packages/ui/acp/src/index.ts:203`](../packages/ui/acp/src/index.ts)
* deployment persona (forwarded to the system-prompt plugin); `toolOrder` is
* the explicit model-facing tool order (forwarded to the system-prompt plugin);
* `tools` is the tool registry's config (its presentation `mode`, forwarded
* through agent-spine-demo); `persistenceRoot` is the JSONL backend's directory.
* through agent-spine-demo); `maxParallelToolCalls` configures the bundled
* agent loop; `persistenceRoot` is the JSONL backend's directory.
*/
export interface Config {
/** Model name for ACP-created agents (must have a registered adapter). */
model: string
/**
* Concurrent parallel-safe tool-call cap for the bundled agent loop. A
* positive integer; the loop defaults it when omitted and `1` is serial.
*/
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). */
@@ -61,7 +67,7 @@ export interface Config {
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
Source: [`packages/examples/acp-demo/src/index.ts:31`](../packages/examples/acp-demo/src/index.ts)
Source: [`packages/examples/acp-demo/src/index.ts:32`](../packages/examples/acp-demo/src/index.ts)
## `@deepseek-ai/dsh-agent-loop`

View File

@@ -4,7 +4,7 @@ Status: implemented
## Problem
An assistant message may contain several sibling `tool-call` blocks. Running them serially adds the latency of independent reads, web requests, and subagent runs even though the model has already requested them together.
An assistant message may contain several sibling `tool-call` blocks. Running them serially adds the latency of independent reads and web requests even though the model has already requested them together.
Concurrency is a host scheduling concern, not model-facing tool metadata. The loop needs to decide which calls may overlap without hardcoding tool names or exposing scheduler policy in the JSON schema.
@@ -58,12 +58,10 @@ Any shared state touched during execution must be concurrency-safe. This include
`maxParallelToolCalls` is a positive AgentLoop deployment cap shared by every agent the factory creates. It defaults to `10`; `1` preserves serial execution. Exact fields and defaults live in the generated [configuration catalog](../../../config-catalog.md).
The shipped declarations are conservative. Web search, web fetch, filesystem read, and foreground subagent calls opt in. Background subagent starts remain exclusive because they register parent-owned task state. Filesystem writes and edits, bash tools, workflow, user interaction, todo mutation, Code Mode, and Cordis mutation tools also remain exclusive. Bash stays exclusive until its owning package supplies a proven input-sensitive classifier.
The shipped declarations are conservative. Web search, web fetch, and filesystem read opt in. Filesystem writes and edits, bash tools, subagent delegation, workflow, user interaction, todo mutation, Code Mode, and Cordis mutation tools remain exclusive. A subagent may share its parent's workspace or external resources, and the unary classifier cannot prove that sibling delegations have disjoint effects. Bash stays exclusive until its owning package supplies a proven input-sensitive classifier.
Filesystem read relies on a narrow recorder exception: its synchronous observation updates may settle out of order, but write and edit re-check the observed version before mutation, so stale state only produces `FS_STALE_VERSION`.
The subagent declaration requires providers to accept concurrent `start()` calls for independent runs. A provider may queue, enforce its own capacity, or return a typed failure instead of requiring the parent loop to serialize every subagent call.
## Verification
Unit coverage pins fail-closed classification, typed argument validation, grouping, barriers, the rolling cap, distinct execution objects, middleware order, ordered results and context, and abort draining. First-party tests pin each parallel declaration.
@@ -98,6 +96,6 @@ Parallel calls may begin in cases where serial execution would have aborted befo
Ordered commits may hold a fast result behind a slow earlier sibling. This preserves replay and model-history order while live surfaces still show pending progress.
Concurrent subagents and external calls can compete for quota or process capacity. Providers own their capacity controls; the loop cap only limits calls from one agent step.
Concurrent external calls can compete for quota or process capacity. Providers own their capacity controls; the loop cap only limits calls from one agent step.
Tool registration is a scheduling boundary. The scheduler currently plans all groups before dispatch, so an earlier registry mutation can make a later classification stale. Binding dispatch to the classified definition or reclassifying after exclusive barriers remains a named correctness gap.

View File

@@ -357,7 +357,7 @@ Source: [`packages/skill/tool-skill/src/index.ts`](../packages/skill/tool-skill/
### `subagent`
Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. You may issue several subagent calls in one message to run independent tasks concurrently when their work scopes do not overlap. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.
Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.
```json
{