Fix async injection tool-result ordering

This commit is contained in:
Hypatia May
2026-07-15 11:12:05 +08:00
parent 34bb75b2d2
commit 4ce32807e1
10 changed files with 104 additions and 55 deletions

View File

@@ -41,7 +41,7 @@ The handle every plugin programs against:
- `agent.send(content, options?)` — queue a message; starts a turn when idle. Content and resolved source become one detached, deeply frozen lossless-JSON record before `agent/queued` and enqueue; invalid data throws synchronously, and caller or notification-listener in-place mutation cannot change the log or model input (`agent/prompt-submit` still rewrites by returning replacement content).
- `agent.steer(content, options?)` — steer a running turn (inject between steps); uses the same owned acceptance boundary and behaves like `send` when idle
- `agent.inject(content, options?)`inject in-session context (context/message event); the next request sees it. Does not run the model. While a turn is open it joins that turn; while idle it is wrapped in a one-shot `injection` turn so every event stays turn-enclosed ([the turn-enclosure invariant](../../../docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md))
- `agent.inject(content, options?)`accept detached in-session context without running the model; the next request sees its `context/message`. While a turn is open it joins that turn, deferring FIFO behind any pending tool-result batch so provider pairing remains contiguous; while idle it is wrapped in a one-shot `injection` turn and durability checkpoint ([the turn-enclosure invariant](../../../docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md)).
- `agent.cancel(reason?)` — cancel ALL pending work: clears the queued + steering FIFOs, aborts the in-flight step, and drops a turn about to start (the pre-step window) so a queued-but-not-started prompt never runs. A UI/ACP `session/cancel` maps to this. The single public stop primitive. Idle with nothing pending → a safe no-op.
- `agent.whenIdle()` — resolve once the agent reaches quiescence after settling out of `running` (idle → immediately; disposed → awaits the loop exit). A non-owner's quiescence-observation hook: it observes the work settling WITHOUT tearing the agent down. Teardown is separate — a lifecycle owner stops and unregisters via `AgentHandle.dispose()`, which awaits the loop exit directly.
- `agent.session`, `agent.status`, `agent.options`, `agent.id`

View File

@@ -103,10 +103,11 @@ export interface Agent {
steer(content: ContentBlock[], options?: SendOptions): void
/**
* Append model-facing context without running the model. Idle injection uses
* a one-shot turn and durability checkpoint, while injection during an open
* turn joins it at the current log position. Disposal awaits idle checkpoints;
* flush failures are reported through `agent/error`, not thrown to the caller.
* Append detached model-facing context without running the model. An open-turn
* injection joins at the current log position unless tool results are pending,
* in which case it waits FIFO until the complete result batch is logged. Idle
* injection uses a one-shot turn and durability checkpoint. Disposal awaits
* idle checkpoints; flush failures report through `agent/error`.
*/
inject(content: ContentBlock[], options?: SendOptions): void