A peer-IRC interrupt (e.g. a subagent message) aborts only interruptible
waits and leaves already-running non-interruptible foreground work alone.
But runTool's `interruptState.triggered` early-return skipped every
not-yet-started tool regardless of source, so a non-interruptible tool
queued behind an interruptible wait in the same batch (a batched todo/write
after `hub wait`) was dropped with "Skipped due to pending peer interrupt".
Exclude non-interruptible tools from the early skip on the IRC path; user
and system steering still preempt all queued work.
Fixes#7493
Publish terminal daemon completions to the session that started the
process so idle agents can resume without polling hub status.
Persist every unacknowledged generation with a stable completion ID and
immutable snapshot. Replay the collection after reconnect or broker
recovery, and clear each event only after the owning client acknowledges
it.
Signed-off-by: Christian Stewart <christian@aperture.us>
- Added shared Python call and literal serialization utilities with multiline verbatim support.
- Standardized tool inventories to format as an OpenAI-Harmony functions namespace using TypeScript declarations.
- Updated tool normalization and rendering functions to accept options objects and default to Python-syntax examples.
- Refactored Gemini dialect rendering to leverage shared serialization functions directly.
- Added `isStreamEnvelopeErrorText` to packages/ai/src/error/flags.ts to recognize stream envelope truncation errors.
- Updated `streamAnthropicOnce` in packages/ai/src/providers/anthropic.ts to throw an envelope error when streams die mid-generation without a terminal frame.
- Updated `recoverTransientErrorToolTurn` in packages/agent/src/agent-loop.ts to recognize Anthropic stream envelope truncation errors for tool call salvage.
Track entry into tool.execute separately from tool event emission. Never-started skips retain SyntheticToolResultDetails with executed:false; in-flight aborts now use distinct interrupted metadata with execution:started so consumers do not assume no partial work occurred.
Keep both interrupt states neutral in the TUI and cover the agent metadata boundary plus rendering behavior.
Fixes#7199
A tool call aborted mid-batch to service queued steering/peer input emits a synthetic placeholder result with isError:true so the model retries it. The TUI keyed all error styling (red ✘, red frame/text) off that flag, so a normal steering skip rendered identically to a real tool failure.
Mark the skip placeholder with the existing SyntheticToolResultDetails discriminator (source: "interrupt_skipped", executed:false) and render benign skips through the neutral generic card (info glyph, dim text, neutral background), bypassing any bespoke error frame. Genuine failures keep their error styling.
Fixes#7199
- Added `prepareToolCallDispatch` and `PreparedToolCall` to handle argument validation and `beforeToolCall` before message snapshotting.
- Implemented `preparedDispatchByMessage` WeakMap to store pre-dispatch results for streamed messages.
- Updated `executeToolCalls` to consume pre-computed dispatch preparation results.
- Updated documentation and changelog to specify `beforeToolCall` timing on the streamed path.
- Added a prepareToolCall phase to the agent loop running before tool scheduling for validation and hooks.
- Updated BeforeToolCallContext and result types to support argument replacement instead of in-place mutation.
- Updated coding-agent extension handling and runner to track emitted tool calls and re-evaluate approvals on input revisions.
- Added comprehensive test coverage for argument replacement, concurrency resolution, and schema validation.
- Cleared the retained soft-requirement lifecycle alongside the deferred
hard choice: clearDeferredToolDirectives() owns both, is called from
clearAllQueues/reset and session-scoped tool-state cleanup, with a
regression covering reminder re-injection after a queue clear.
- Allowed void-returning pre-model gates via the named AgentBeforeModelCall
type and normalized gate results in the loop and Agent dispatcher.
- Documented that the first gate installed mid-run applies from the next
run; corrected the onToolChoiceRejected contract docs; documented the
cross-run lifetime of ToolChoiceQueue's in-flight claim.
- Removed the unused addBeforeModelContextBuild hook.
- Relocated both packages' changelog entries out of the released 17.1.4
sections into Unreleased with PR attribution, folding the never-shipped
Fixed bullet into Added and noting the input-event timing change.
A Harmony retry keeps its logical turn open while the next provider call is prepared and gated. An abort during that gate previously ended the agent stream directly, leaving observers with an unmatched turn_start event.
Route the aborted gate through the existing pre-model stop owner so it emits the synthetic aborted message and closes the open turn before ending the stream. Fresh turns retain their existing no-provider-call cancellation behavior.
Signed-off-by: Christian Stewart <christian@aperture.us>
The agent loop had no place to refuse a provider request. A host that needs to
act on the assembled context before it is billed, checking that the prompt still
fits the window, that a budget boundary has not been crossed, or that the
session should hand off instead of spending, could only observe the request
after the fact, when the tokens were already committed.
Add `AgentLoopConfig.beforeModelCall`, asked once per turn beside the deadline
check and before `turn_start` is emitted. A `stop` result ends the stream with
no turn open, so nothing has to synthesise a cancellation event and no consumer
is left holding a half-open turn. Placing it there also keeps `turn_end`'s
contract intact: that event carries the assistant message for a completed turn,
and a gated stop has no assistant message to report.
`syncContextBeforeModelCall` keeps its existing void contract and its job of
refreshing prompt and tool state, so implementations typed as returning void are
unaffected.
`Agent.setBeforeModelCall` installs the host's callback, and `addBeforeModelCall`
registers an additional callback without displacing the host's, returning a
disposer so an extension can attach and detach independently. A supplied
`reason` is logged where the loop stops.
Signed-off-by: Christian Stewart <christian@aperture.us>
While a turn is running, the loop watched for out-of-band steering by
re-checking the queue on a fixed interval. The interval sets a floor on how long
a message waits and burns a wakeup on every tick that finds nothing, and the
delay is worst exactly when it matters: a cancellation or a correction issued
during a long tool loop sits until the next tick.
Wait on the queue instead. `Agent` keeps a set of steering waiters and notifies
them whenever a message is enqueued, and the loop awaits
`waitForSteeringMessages`, re-checking only when woken. The timer path remains
for the IRC interrupt queue, which is session-owned and has no wake callback, so
nothing regresses where no event source exists.
Every wait races against local abort. The callback contract does not require an
implementation to observe the signal, and one that resolves only on the next
queue event would otherwise never settle once a batch finished, so awaiting it
during teardown would hang a batch that simply had no steer.
Which controllers a steer raises is unchanged: this is a timing change, not a
change to what an interrupt does.
Signed-off-by: Christian Stewart <christian@aperture.us>
- Add `resolveFallbackTool` callback to `AgentOptions` and `AgentLoopConfig` that resolves tool calls not found in the advertised set.
- Use the callback as a third lookup step after `name` and `customWireName` match, enabling side transports like `xd://` device mounts.
- Add test coverage verifying the fallback resolves known devices and preserves "not found" errors for unknown names.
- Wire the coding agent's device registry as `resolveFallbackTool` in both `createAgentSession` and `streamAgentSession` paths.
Persisted native server-tool calls and web-search results in assistant content, replayed them only to the issuing Anthropic provider, and retained Umans gateway filtering.
Fixes#6495
A stream that stalls or aborts mid-tool-call ends the assistant turn with
stopReason error/aborted, then appends a synthetic tool_result per un-run
tool call to keep the provider's tool_use/tool_result pairing intact. That
placeholder trailed the failed turn, so AgentSession.retry() — which only
inspected the last message and required role assistant — short-circuited to
false and /retry printed 'Nothing to retry'.
retry() now walks back over trailing synthetic tool results (details
__synthetic true) before the assistant + stopReason check, stripping both
the placeholders and the failed turn. Only synthetic results are skipped, so
a turn whose tools actually ran stays non-retryable. Adds an exported
isSyntheticToolResultMessage guard in agent-loop.ts.
Fixes#6056
Three-piece architecture so subagents inherit async.enabled and
bash.autoBackground.enabled instead of having both force-disabled:
- Owner-routed delivery: AsyncJobManager gains registerDeliverySink /
waitForOwnerJobs; every AgentSession registers a sink for its own agent
id, so background job results inject into the owning agent's run.
Owned deliveries with no live sink dead-letter (result retained on the
job row) instead of misrouting into the first top-level session.
- Quiescence barrier: a subagent's final yield with owner jobs still
running/undelivered is a scheduling pause, not completion. The run
driver notifies the model once (hub wait/cancel), settles owner work,
and folds results in as async-result follow-ups; teardown cancels and
awaits surviving jobs before isolation worktree capture/cleanup.
- Steering soft channel: queued steering no longer hard-aborts
non-interruptible tools; it aborts interruptible waits and raises a
cooperative ToolCallContext.steeringSignal. The mid-batch watch runs
for every batch, and auto-backgroundable bash backgrounds itself on
steer so incoming messages inject promptly with no work lost.
Resolved tool interruptibility from each call's raw arguments so mixed-operation tools can keep side-effecting calls non-interruptible.
Restricted the unified hub to interrupt passive waits and followed logs while preserving start, send, and lifecycle operation results.
Fixes#5995
A dropped stream that emitted toolcall_start/delta but never toolcall_end
leaves an incomplete toolCall block in content. The prior guard bailed on
any toolCall block, so the outer loop dispatched empty or partially
parsed arguments instead of retrying the transport failure.
Track streamed tool-call ids (toolcall_start/delta) alongside completed
ones (toolcall_end): a call streamed but never completed is incomplete.
reclassifyEmptyToolUseStop now reclassifies unless a usable (atomic or
completed) tool call remains, stripping incomplete blocks first. Atomic
deliveries (single done/end(result) message, e.g. Cursor) emit no
granular events and stay usable.
Fixes#5600
Streams finalized via end(result) with no terminal done/error event fall
through to the trailing-result branch, which returned response.result()
unchanged. An empty toolUse turn completing that way stayed a silent
success and never retried. Apply the same
retainCompletedToolCalls/recoverTransientErrorToolTurn/
reclassifyEmptyToolUseStop chain to the trailing branch.
Fixes#5600
A provider stream that closes after the thinking block but before the
tool call JSON is emitted finalizes with stopReason=toolUse and zero
toolCall content blocks. The loop treated this as a successful turn:
dispatched no tools, rendered an empty tool widget, and never retried.
reclassifyEmptyToolUseStop stamps such a turn as stopReason=error with
the Transient classifier bit set explicitly, so AgentSession's standard
retry-with-backoff path fires regardless of message-text matching.
Fixes#5600
- Added the `xd://` virtual device protocol (`internal-urls/xd-protocol.ts`, `tools/xdev.ts`): tools declaring `loadMode: "discoverable"` are unmounted from the request tools array and driven via `read xd://` (list/docs+schema) and `write xd://<tool>` (execute), gated by the `tools.xdev` setting (default on) and inlined into the system prompt.
- Merged the `irc`, `job`, and `launch` tools into a single `hub` tool (`tools/hub/`, `async/job-manager.ts`): messaging keeps `send`/`inbox`/`list`, job control maps to `wait`/`cancel`/`jobs`, process supervision keeps `start`/`logs`/`stop`/`restart`/`describe` with `ps`, and the unified `wait` races background jobs against peer messages; SDK `IrcTool`/`JobTool`/`LaunchTool` are replaced by `HubTool`.
- Removed the hidden `resolve` tool in favor of the `xd://resolve`/`xd://reject`/`xd://propose` resolution devices, auto-including `write` whenever a deferrable tool or plan mode is present.
- Removed the BM25 tool-discovery system: the `search_tool_bm25` tool, the `tool-discovery` module, the `tools.discoveryMode`/`mcp.discoveryMode`/`mcp.discoveryDefaultServers`/`tools.essentialOverride` settings, per-tool MCP selection, and the `mcp_tool_selection` message type.
- Unified tool presentation on `ToolLoadMode` (`essential`|`discoverable`), replacing the custom-tool `xdev?: boolean` opt-out; custom, extension, MCP, RPC host, image-generation, and TTS tools now default to `discoverable`, and added a `satisfies` predicate to `SoftToolRequirement`.
- Removed the standalone `ssh` command tool and `ssh/ssh-executor` (the `ssh://` read/write/search protocol stays), and made `--tools` address hidden built-ins.
- Updated collab-web to render `xd://` dispatches and `hub` op families, dropped the `search_tool_bm25`/`ssh`/`report-finding` renderers, refreshed tool docs and prompts, and migrated the affected tests and changelogs.
The interrupt-clobber branch in executeToolCalls replaced a tool's real
result with the "Skipped due to queued user message" placeholder whenever
an interrupt fired, the tool's own signal aborted, and the result was an
error. It ignored whether tool.execute() actually completed, so steering
while a tool was in flight could discard a genuine error result (e.g. a
command exiting non-zero) that the tool had already produced.
Gate the clobber on !completedToolExecution so a tool that ran to
completion keeps its real result; only tools cut off before returning are
reported as skipped. Align the aborted telemetry status the same way.
Fixes#4752
- Disabled the eval watchdog when timeout is explicitly zero.
- Classified session deadline aborts as TimeoutError while preserving their message.
- Documented and tested both timeout contracts.
Fixes#5250
- Normalized completed image_generation_call results into assistant image blocks.
- Persisted image bytes through the session blob store and rendered them in live, replay, ACP, proxy, telemetry, and HTML paths.
- Added response normalization, persistence, and TUI rendering regressions.
Fixes#4768
- Introduced an `AgentPauseGate` mechanism to suspend and resume agent loops and tool executions safely.
- Added a `/pause` slash command to trigger a new fullscreen UI that manages agent suspension and lifecycle.
- Integrated pause checks into the core agent loop and tool execution pipeline to ensure responsive state handling.
- Provided a new pause screen component to facilitate user interaction and resume control during suspension.
- Aborted the active agent loop synchronously when a terminal yield tool result finishes, so IRC-wake turns stop before another provider call.
- Added a regression covering idle IRC wake handling after a terminal yield.
Fixes#4963
- derive per-tool abort labels from a tool-scoped abort signal for provider-built aborted messages
- restore main's single-call TTSR label test dropped by the merge
- complete the innocent read in the sibling-label test; incomplete matched calls mint no placeholder under the retention policy