createXtmlRecoveryStreamParser()now recognizes an empty channel name in malformed<|open|><|sep|>/<|close|><|sep|>transitions, including markers split at every internal stream boundary, instead of leaking them as visible text.recoverKimiXtmlThinking()now stripstools, other valid named channels, and bare XTML open/close/sep tokens while preserving the existing response/think state changes and code-fenced protocol examples.- The deterministic Kimi mock-loop fixture includes the observed unnamed-close + tools tail so real CLI QA fails if either client recovery layer regresses.
shouldRecoverTextToolCalls()now defaults on for Kimi-family model ids (boundary regex, mirroring the Claude default), so Kimi models get the same normal-mode auto-correction Claude has when native tool calling misfires into plain text.- New
createXtmlRecoveryStreamParser()(protocols/kimi-xtml/recovery-stream.ts): recovers leaked<|open|>tools<|sep|>blocks into canonical tool calls and strips stray XTML channel-transition markers (think/response/message) from visible text; code-masked spans stay untouched;interrupt()restores retained marker fragments as plain text like the invoke parser. wrapStreamWithInvokeRecovery()andRecoveryTextProjectionaccept an optional recovery-parser factory (default unchanged: ANTML invoke).ModelRuntimeselects the XTML factory for Kimi models viahasKimiTextToolCallRecovery(); Claude and non-Kimi behavior is byte-identical.
- Recovery must observe raw provider-neutral assistant text before the coding-agent loop executes tools, and the parser selection must happen inside the stream wrapper, ahead of extension-observable events.
- Added the
kimi-xtmltext-tool protocol: Kimi K3's native XTML channel format (<|open|>name<|sep|>/<|close|>name<|sep|>markers; tools blocks containingcallchannels withargumentchannels), so Kimi models can serve text-encoded tool calling through a syntax they are already trained on. - Typed argument coercion: string (default), number, boolean, object/array (strict JSON; malformed values reject the call via
onErrorinstead of coercing). - Streaming parser reassembles markers split across chunks, emits snapshot argument deltas per completed argument, and finalizes unterminated calls as
incompletewith the arguments parsed so far. Unknown tools are skipped with diagnostics. - Registered in the
ToolCallFormatunion, thegetToolCallFormat()whitelist, and the protocol registry; compat docs and TESTING.md updated.
- Text tool-call protocols must intercept and re-encode the provider stream and message history inside the middleware, before coding-agent extensions observe assistant events.
- Added provider-neutral Claude text tool-call recovery for leaked bare or exact lowercase
antml:invokes, with strict tag/namespace recognition, eager bounded parsing, and validation-gated argument coercion. - Recovery scans only ordinary assistant text. Inline/fenced code, thinking/redacted thinking, provider-native blocks, native tool calls, and historical messages remain untouched.
- Mixed native/recovered content preserves source order, IDs, signatures, metadata, usage/cost snapshots, and stable partial indices. Native-first ID conflicts are skipped; late collisions and malformed native event order fail closed.
- Complete calls execute through the normal agent loop and persist as native history. Incomplete, invalid, overflowed, aborted, or collided calls remain non-executable and produce redacted diagnostics.
- Parser, code-mask, wrapper, metadata, termination, collision, UTF-16/resource, persistence, and provider replay behavior is covered by focused regression and standalone surface drivers.
- Recovery must observe raw provider-neutral assistant text before the coding-agent loop executes tools, while preserving per-event partials, provider-native ordering, cancellation, and transport metadata.
- Invoke recovery now keeps abort terminals interrupted, strips executable calls from aborted or consumer-cancelled messages, and never converts them into successful
toolUseturns. - One terminal path handles done, transport errors, iterator failures, exhaustion, and cancellation after flushing each active text parser exactly once. Dangling recovered calls finalize as incomplete and non-executable.
- Recovery events now snapshot the corresponding source message metadata and complete usage/cost objects, preventing later partial mutations from rewriting previously emitted event metadata. The snapshotter recursively clones enumerable plain objects and arrays with cycle/alias preservation while retaining exotic values by reference.
- Consumer cancellation now awaits upstream iterator cleanup, shares one promise across concurrent returns, and is disabled once a natural terminal or wrapper-owned IteratorClose begins. The legacy text-protocol wrapper remains unchanged.
- Termination, iterator, partial-message, and usage fidelity are properties of the provider-neutral stream wrapper before coding-agent extensions receive assistant events.
- Added the
antmltext-tool protocol: the ANTML<function_calls>/<invoke>/<parameter>format Anthropic models are post-trained on (see "Better Models: Worse Tools", https://lucumr.pocoo.org/2026/7/4/better-models-worse-tools/). Newer Claude models (Opus 4.8, Sonnet 5) emit byte-correct tool calls but append invented keys (requireUnique,oldText2,type, ...), substitute parameter aliases (pathforfile_path,old_strforold_string), and occasionally produce broken\uXXXXescapes — slop that Claude Code's own harness absorbs silently while strict parsers reject the whole call. - The protocol shares the invoke scanner/stream machinery with
anthropic-xmlvia a newInvokeProtocolConfig(protocol id, label, tool-call id prefix, coercion strategy);parse.ts/stream.tsinprotocols/anthropic-xml/were parameterized with zero behavior change for the existing format (messages, ids, and event sequences are byte-identical). antmlcoercion (protocols/antml/coerce-parameters.ts) applies validation-gated repairs: case/separator-insensitive property matching, a documented alias table, recursive unknown-key filtering honoringadditionalProperties,\uescape repair plus lone-surrogate replacement, lenient scalar spellings (" TRUE ",'"2"'), and duplicate-parameter last-wins. Every repaired record must still passValue.Checkagainst the tool schema — repairs only narrow input, never invent it, keeping the middleware's strict-parsing contract.- Formatted output wraps the canonical invoke serialization in
<function_calls>; input accepts both wrapped and bare invokes. Truncation-at-finish recovery inherits the R1-R4 boundary from the shared stream core.
- The five text-based tool-call protocols (
anthropic-xml,hermes/json-mix,yaml-xml,morph-xml,gemma4-delimiter) handled a tool call truncated at stream end inconsistently: some parsers silently dropped the partial call, some leaked the raw markup as text, andmorph-xmlexecuted a stale argument snapshot that no longer reflected what the model was writing. There was no shared definition of "recoverable" atfinish(), so the same truncation could be dropped, leaked, or executed stale depending on the format. - Every protocol
finish()now applies one normative recoverability boundary (R1-R4): the opening marker is complete (R1), the tool name resolves against the suppliedtools(R2), the body is structurally complete except for a missing closing marker or a proper prefix of it plus trailing whitespace (R3), and the recovered arguments passvalidateToolArgumentsagainst the tool schema (R4). A call is recoverable iff all four hold; recoverable calls are ALWAYS force-completed and executed exactly like a complete call, withvalidateToolArgumentsgating every forced completion. - Unrecoverable calls with a readable, resolving name now emit a terminal
toolcall_endcarryingincomplete: trueanderrorMessage(plus best-effort parsed arguments) instead of being dropped or leaked. Nameless protocol fragments (no readable tool name, so not representable as aToolCall) are dropped with a metadata-onlyonErrordiagnostic ({ protocol, retainedLength }). The raw truncated fragment is never emitted as text, placed inonErrormetadata, logs, tool results, or replayed context — even whenemitRawToolCallTextOnErroris set, for the truncated-at-finish case. Mid-stream malformed-COMPLETE-call text-emission policy is unchanged. morph-xml'slastArgumentsSnapshotfallback is demoted from "execute stale args" to "flagged incomplete": a snapshot that no longer matches the live buffer is not what the model was writing and must not be executed.- The
incomplete/errorMessageflags propagate from the parser event through the stream wrapper into the agent loop, where a flagged call becomes an immediate error tool result that keeps the inner loop alive so the model re-issues the tool call on the next assistant turn (the retry contract). "morph-xml"is the canonical format id;"xml"remains as a deprecated alias resolving to the same protocol, so persistedmodels.jsonconfigs and compiled consumers keep working.
- The recoverability boundary, per-format
finish()behavior, the no-raw-fragment guarantee, and theincomplete-flag propagation all live inside the provider-agnostic tool-call middleware before any coding-agent extension participates.
- Refactor the built-in text-based tool-call protocols toward the
minpeter/ai-sdk-tool-call-middlewarearchitecture. - Focus areas:
morph xmlparsing/streaming should stop manufacturing invalid JS values from malformed XML.hermesshould move toward a shared JSON-mix style parser/stream model.yaml+xmlsupport should be added with minimal surface-area changes.
- Completed:
morph xmlnow rejects malformedarray<object>payloads instead of coercing them into invalid strings.hermesnow delegates parsing/streaming to a shared JSON-mix helper so delimiter-based protocols can share logic with less drift.yaml+xmlsupport is now wired into the protocol registry with parser/formatter/stream coverage.- The stream wrapper now preserves reconstructed outer tool-call/text content across provider-side stream errors instead of falling back to the raw provider message.
- When a transport error happens after complete tool-call blocks were already recovered, the wrapper now finishes the turn as
toolUseso the agent can execute those tools instead of dropping the whole turn.
packages/ai/src/tool-call-middleware/protocols/morph-xml.tspackages/ai/src/tool-call-middleware/protocols/hermes.tspackages/ai/src/tool-call-middleware/context-transformer.tspackages/ai/src/tool-call-middleware/types.tspackages/ai/src/tool-call-middleware/index.tspackages/ai/src/tool-call-middleware/stream-wrapper.tspackages/ai/test/tool-call-middleware/*
- The defect is in the provider-agnostic tool-call parsing layer inside
packages/ai, not in coding-agent UX glue. - Fixing malformed XML coercion, streaming parser behavior, and protocol registration requires changes to shared core parsing logic.
packages/ai/src/tool-call-middleware/protocols/*packages/ai/src/tool-call-middleware/types.tspackages/ai/test/tool-call-middleware/*
- Added the
anthropic-xmltext-tool protocol for OpenAI-compatible models that emit legacy Anthropic-style<invoke name="..."><parameter name="...">...</parameter></invoke>calls. - Registered the format in the middleware protocol registry, compatibility whitelist, and coding-agent custom-model schema.
- Added batch, streaming, resource-bound, formatter, coercion, registration, and faux-provider end-to-end coverage.
- Text-tool parsing and custom-model format validation happen in shared AI and coding-agent core before extension tool execution.