feat(tools): live dispatch lifecycle + native-contract parallel sub-calls in Code Mode

The bridge replaces its serialization queue with a pool that reuses the
native concurrency contract: submissions classify through
registry.executionMode (fail-closed isConcurrencySafe), start strictly in
submission order, overlap up to the validated maxParallelSubCalls config
(default 10; 1 restores serial), and exclusive calls drain the pool, run
alone, and bar later calls. Each started sub-call logs a
tool/code-dispatch-start event at pool entry; the existing
tool/code-dispatch settles the pair (started ⇔ settles exactly once;
abandoned queued calls log neither). SDK prompt guidance now states the
true Promise.all contract — re-recorded across every code/both-mode
snapshot (plus the stale cordis-dynamic-toolchain fixture gaining the
required description arg).

Client: CodeSubCall widens to RunningToolCall | ToolResultNode — starts
land the running shape (rows wear the native running ring), settles
replace in place preserving start order, callTime pairs to the start
time. Fixture emits start/settle pairs; jsdom pins the running sub-row;
runtime specs pin in-place settlement and out-of-order completion.
This commit is contained in:
Tianyi Cui
2026-07-26 06:02:36 +08:00
parent 526651cb88
commit 8a79679489
34 files changed
+1921 -1673

No files matched your search

@@ -128,15 +128,19 @@ export type ConversationNode =
| UnknownSurfaceNode
/**
* One `run_code` sub-dispatch materialized as a {@link ToolResultNode} so every
* consumer (tool rows, details panel) renders it through the exact components
* that render a native settled call. Never part of the surface `nodes` flow —
* sub-calls live under their parent via {@link ConversationSnapshot.codeDispatches}.
* `callId` is the deterministic sub-call id (`<parent>:code:<n>`); `call`
* carries the sub-tool name and its JSON-stringified logged arguments;
* `content`/`isError` are the sub-call's complete logged outcome.
* One `run_code` sub-dispatch materialized in the native call-block shapes so
* every consumer (tool rows, details panel) renders it through the exact
* components that render a native call: a started-but-unsettled sub-call is a
* {@link RunningToolCall} (rows derive the running state from the shape,
* exactly as for native calls) and its `tool/code-dispatch` settlement
* replaces it in place with the {@link ToolResultNode} form. Never part of
* the surface `nodes` flow — sub-calls live under their parent via
* {@link ConversationSnapshot.codeDispatches}. `callId` is the deterministic
* sub-call id (`<parent>:code:<n>`); the call side carries the sub-tool name
* and its JSON-stringified logged arguments; `content`/`isError` are the
* settled sub-call's complete logged outcome.
*/
export type CodeSubCall = ToolResultNode
export type CodeSubCall = RunningToolCall | ToolResultNode
/** In-flight tool card material: tool/call seen, tool/result not yet. */
export interface RunningToolCall {
@@ -616,14 +616,36 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
/** Per-event side effects (right column of the §A.9 dispatch table):
* chunk accumulation / partial clear on finalize / openCalls add-remove. */
private applyEventSideEffects(event: SessionEvent, view?: ToolEventView): void {
// `tool/code-dispatch` is declared by the host-side dsh-tools plugin whose
// types cannot enter the client program (its host Context merges collide
// with the client's), so this wire consumer narrows it structurally —
// the same posture as every other cross-wire event payload.
// The `tool/code-dispatch-start`/`tool/code-dispatch` pair is declared by
// the host-side dsh-tools plugin whose types cannot enter the client
// program (its host Context merges collide with the client's), so this
// wire consumer narrows them structurally — the same posture as every
// other cross-wire event payload.
if ((event.type as string) === 'tool/code-dispatch-start') {
// A started sub-dispatch enters the index as a RunningToolCall — the
// exact shape a native in-flight call renders from — under its parent
// run_code callId; it never joins the surface flow.
const data = event.data as unknown as {
parentCallId: string
subCallId: string
name: string
arguments: unknown
}
const running: CodeSubCall = {
callId: data.subCallId, name: data.name,
argsRaw: JSON.stringify(data.arguments),
turn: 0, step: 0, time: event.time, callView: null,
}
const siblings = this.codeDispatches.get(data.parentCallId) ?? []
this.codeDispatches.set(data.parentCallId, [...siblings, running])
this.dispatchesRev++
return
}
if ((event.type as string) === 'tool/code-dispatch') {
// A sub-dispatch becomes a ToolResultNode so rows and the details
// panel reuse the native rendering path verbatim; it indexes under its
// parent run_code callId and never joins the surface flow.
// Settlement replaces the running entry in place (same array position,
// so parallel sub-calls keep their start order) with the
// ToolResultNode form; a settle with no observed start (history window
// cut mid-pair, or a pre-start-event log) appends directly.
const data = event.data as unknown as {
parentCallId: string
subCallId: string
@@ -632,17 +654,22 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
isError: boolean
content: ContentBlock[]
}
const parent = data.parentCallId
const siblings = this.codeDispatches.get(parent) ?? []
const sub: CodeSubCall = {
const siblings = this.codeDispatches.get(data.parentCallId) ?? []
const at = siblings.findIndex(sub => sub.callId === data.subCallId)
const started = at === -1 ? undefined : siblings[at]
const settled: CodeSubCall = {
kind: 'tool-result', seq: event.seq, time: event.time,
callId: data.subCallId,
call: { name: data.name, argsRaw: JSON.stringify(data.arguments) },
callTime: event.time,
// Duration source: the paired start's time when observed.
callTime: started === undefined ? event.time : started.time,
content: data.content, isError: data.isError,
callView: null, resultView: null,
}
this.codeDispatches.set(parent, [...siblings, sub])
this.codeDispatches.set(
data.parentCallId,
at === -1 ? [...siblings, settled] : siblings.map((sub, index) => (index === at ? settled : sub)),
)
this.dispatchesRev++
return
}