feat(contract)!: tool-call parse-failure + truncation diagnostics#1875
Conversation
|
Orchestrator review (adversarial): approve. Freeze handling is correct — both new cases ( |
Add two non-fatal GenerationEvent diagnostics so hosts can observe tool calls that previously vanished silently in ToolCallTransform: - #1857 .toolCallParseFailed(rawBody:): when a delimited open/close marker pair surrounds a body the dialect parser rejects (parseBody returns nil), emit a diagnostic carrying the raw body instead of dropping the call with no event. Hosts can now distinguish "broken tool call" from "no tool call". - #1858 .toolCallTruncated(rawBody:): opt-in via ToolCallTransform(markers:surfaceTruncatedToolBody:) (default false, so default behavior is unchanged). When enabled, finalize() and the body-size cap surface the buffered partial body of an unterminated tool block so a mid-stream truncation is observable rather than silently discarded. Both follow the throttleDiagnostic(reason:) precedent — advisory metadata, Sendable/Equatable String payloads, no chat-message state mutation. Freeze hygiene: the GenerationEvent "Vocabulary freeze (1.0)" header is updated to list the new cases. Every exhaustive switch over GenerationEvent across Sources/ and Tests/ gains the new arms (12 sites: GenerationStream- Consumer, EventRecorder, ScenarioRunner, the APIFreeze BackendSeamConsumer freeze fixture, and 8 backend/contract test switches). api-breakage-allowlist gains the two new-enum-case lines plus the ToolCallTransform.init signature change (defaulted param; existing markers: callers still compile). Digester passes locally with exit 0. BREAKING CHANGE: GenerationEvent gains .toolCallParseFailed(rawBody:) and .toolCallTruncated(rawBody:); exhaustive switches over GenerationEvent without a default/@unknown default arm must add handling for the new cases. Resolves #1857 Resolves #1858 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6c7cef8 to
fe3461d
Compare
What
Adds two non-fatal
GenerationEventdiagnostics so hosts can observe tool calls that previously vanished silently insideToolCallTransform(ManifoldContract).#1857 —
.toolCallParseFailed(rawBody:)When a well-formed open/close marker pair surrounds a body the dialect parser rejects (
parseBodyreturnsnil), the transform now emits a non-fatal diagnostic carrying the raw body instead of dropping the call with no event. Hosts can finally distinguish "model emitted a broken tool call" from "model emitted no tool call" and recover/report.#1858 —
.toolCallTruncated(rawBody:)(opt-in)ToolCallTransform.finalize()(and the body-size cap) discarded an open-but-unclosed tool block including all buffered body text, so a truncated tool call disappeared silently. New opt-in seam:Default is
false— default behavior is unchanged (still drops silently). When enabled, the partial body is surfaced as a.toolCallTruncated(rawBody:)diagnostic so a mid-stream truncation is observable.Both cases follow the existing
throttleDiagnostic(reason:)precedent: advisory metadata,Sendable/EquatableStringpayloads, no chat-message state mutation.Freeze interaction
GenerationEventcarries a "Vocabulary freeze (1.0)" header. We are pre-1.0 (0.51), so completing the vocabulary is appropriatefeat!:work. Done with full freeze hygiene:switchoverGenerationEventacrossSources/andTests/updated (12 sites):GenerationStreamConsumer,EventRecorder,ScenarioRunner, theAPIFreezeTests/BackendSeamConsumerfreeze fixture, and 8 backend/contract test switches (ToolCallContractTests×2,ParallelToolCallOrderingTests,Claude/Ollama/OpenAI/OpenAIResponsesstream-extractoreventKeyswitches,CloudThinkingTokenTests,OpenAIResponsesBackendTests,OllamaToolCallLiveReplayTests). Core-internal switches stay exhaustive (new arms added); none introduced a new@unknown defaultconvention..github/api-breakage-allowlist.txt: twoenumelement ... has been added as a new enum caselines (matching thegenerationCompletedprecedent) plus a line for theToolCallTransform.init(markers:)signature change (added a defaulted parameter; existingmarkers:-only callers still compile). Digester run locally → exit 0.Tests
In
OutputParserSessionTests:.toolCallParseFailedwith the raw body (was: vanished); a parse failure does not poison a following valid call.Sabotage checks were used during development and removed before commit.
Verification
swift build --build-tests— cleanswift build --build-tests --traits Server,Macros— clean (switched-enum trait sweep)OutputParserSessionTestsslice: 18/18 passManifoldContract): exit 0 with updated allowlistBREAKING CHANGE:
GenerationEventgains.toolCallParseFailed(rawBody:)and.toolCallTruncated(rawBody:); exhaustive switches overGenerationEventwithout adefault/@unknown defaultarm must add handling for the new cases.Resolves #1857
Resolves #1858