From 28488a4e356fd1fdf4820873b8d9ca50c493253e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 21 Aug 2026 18:27:42 -0400 Subject: [PATCH 1/6] fix(core): recover partial provider failures --- packages/core/src/session/runner/llm.ts | 15 ++-- packages/core/test/session-runner.test.ts | 93 ++++++++++++++++++++--- 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 8f73761c8196..8fd6106c9f96 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -61,10 +61,13 @@ const isDecline = ( ): error is Permission.DeclinedError | QuestionTool.CancelledError => error._tag === "Permission.DeclinedError" || error._tag === "QuestionTool.CancelledError" -const isInterruptedStream = (failure: AIError) => { - if (failure.reason._tag === "InvalidProviderOutput") - return failure.reason.classification === "incomplete-stream" +const shouldContinueAfterFailure = (failure: AIError, record: StepRecord) => { + if (failure.reason._tag === "InvalidProviderOutput") return failure.reason.classification === "incomplete-stream" if (failure.reason._tag === "Transport") return failure.reason.operation === "read" + // A fresh generation can continue partial output, but not cross a tool boundary + // whose provider-side effects may be ambiguous. + if (failure.reason._tag === "ProviderInternal") + return SessionRunnerRetry.isRetryable(failure) && record.calls.length === 0 return false } @@ -120,7 +123,7 @@ const classifyToolExits = ( const TOOLS_INTERRUPTED = { type: "aborted", message: "Tool execution interrupted" } as const const STEP_INTERRUPTED = { type: "aborted", message: "Step interrupted" } as const const RESULT_MISSING = { type: "tool.result-missing", message: "Provider did not return a tool result" } as const -const CONTINUE_AFTER_INCOMPLETE_STREAM = +const CONTINUE_AFTER_PARTIAL_FAILURE = "The previous response was interrupted. Continue from where you left off without repeating completed content." const layer = Layer.effect( @@ -302,7 +305,7 @@ const layer = Layer.effect( ).pipe(Pull.catchDone(() => Effect.fail(outcome.cause))) yield* bus.publish(SessionEvent.Synthetic, { sessionID, - text: CONTINUE_AFTER_INCOMPLETE_STREAM, + text: CONTINUE_AFTER_PARTIAL_FAILURE, }) assistantMessageID = SessionMessage.ID.create() } @@ -597,7 +600,7 @@ const layer = Layer.effect( if ( llmFailure && llmError && - isInterruptedStream(llmFailure) && + shouldContinueAfterFailure(llmFailure, record) && record.outputStarted && tools.declines.length === 0 && !tools.interrupted diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index b44f15d4b55b..d1a5548a8d80 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -10,6 +10,7 @@ import { TransportReason, InvalidProviderOutputReason, InvalidRequestReason, + ProviderInternalReason, RateLimitReason, } from "@opencode-ai/ai" import * as OpenAIChat from "@opencode-ai/ai/protocols/openai-chat" @@ -572,6 +573,13 @@ const streamDisconnected = () => }), }) +const providerInternal = () => + new AIError({ + module: "test", + method: "stream", + reason: new ProviderInternalReason({ message: "The provider failed while generating a response" }), + }) + const continuationRejected = (recovery: "retry-full" | "rotate-and-retry-full") => new AIError({ module: "test", @@ -596,7 +604,7 @@ const incompleteStream = () => }), }) -const INCOMPLETE_STREAM_CONTINUATION = +const PARTIAL_FAILURE_CONTINUATION = "The previous response was interrupted. Continue from where you left off without repeating completed content." const invalidRequest = () => @@ -4591,7 +4599,7 @@ describe("SessionRunnerLLM", () => { content: [ { type: "text", - text: INCOMPLETE_STREAM_CONTINUATION, + text: PARTIAL_FAILURE_CONTINUATION, }, ], }) @@ -4606,7 +4614,7 @@ describe("SessionRunnerLLM", () => { }, { type: "synthetic", - text: INCOMPLETE_STREAM_CONTINUATION, + text: PARTIAL_FAILURE_CONTINUATION, }, { type: "assistant", finish: "stop", content: [{ type: "text", text: " continuation" }] }, ]) @@ -4641,12 +4649,12 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(2) expect(requests[1]?.messages.at(-1)).toMatchObject({ role: "user", - content: [{ type: "text", text: INCOMPLETE_STREAM_CONTINUATION }], + content: [{ type: "text", text: PARTIAL_FAILURE_CONTINUATION }], }) expect(yield* session.context(sessionID)).toMatchObject([ { type: "user" }, { type: "assistant", finish: "error", content: [{ type: "text", text: "Partial" }] }, - { type: "synthetic", text: INCOMPLETE_STREAM_CONTINUATION }, + { type: "synthetic", text: PARTIAL_FAILURE_CONTINUATION }, { type: "assistant", finish: "stop", content: [{ type: "text", text: " continuation" }] }, ]) }), @@ -4680,7 +4688,7 @@ describe("SessionRunnerLLM", () => { content: [ { type: "text", - text: INCOMPLETE_STREAM_CONTINUATION, + text: PARTIAL_FAILURE_CONTINUATION, }, ], }) @@ -4720,7 +4728,7 @@ describe("SessionRunnerLLM", () => { expect(yield* recordedEventTypes(sessionID)).toContain("session.retry.scheduled.1") expect(requests[1]?.messages.slice(-2)).toMatchObject([ { role: "user", content: [{ type: "text", text: "Recover disconnected reasoning" }] }, - { role: "user", content: [{ type: "text", text: INCOMPLETE_STREAM_CONTINUATION }] }, + { role: "user", content: [{ type: "text", text: PARTIAL_FAILURE_CONTINUATION }] }, ]) expect(yield* session.context(sessionID)).toMatchObject([ { type: "user" }, @@ -4735,12 +4743,77 @@ describe("SessionRunnerLLM", () => { }, ], }, - { type: "synthetic", text: INCOMPLETE_STREAM_CONTINUATION }, + { type: "synthetic", text: PARTIAL_FAILURE_CONTINUATION }, { type: "assistant", finish: "stop", content: [{ type: "text", text: "Recovered" }] }, ]) }), ) + it.effect("continues a provider failure after reasoning without tool activity", () => + Effect.gen(function* () { + const session = yield* setup + yield* admit(session, "Recover failed reasoning") + const failure = providerInternal() + yield* TestLLM.push( + TestLLM.failAfter( + failure, + LLMEvent.stepStart({ index: 0 }), + LLMEvent.reasoningStart({ id: "failed-reasoning" }), + LLMEvent.reasoningEnd({ + id: "failed-reasoning", + providerMetadata: { + openai: { itemId: "rs_failed", reasoningEncryptedContent: "failed-encrypted-state" }, + }, + }), + ), + ) + yield* TestLLM.push(TestLLM.text("Recovered", "provider-failure-recovery")) + + const run = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* TestLLM.wait(1) + yield* TestClock.adjust("2400 millis") + yield* Fiber.join(run) + + expect(requests).toHaveLength(2) + expect(requests[1]?.messages).toHaveLength(2) + expect(requests[1]?.messages.slice(-2)).toMatchObject([ + { role: "user", content: [{ type: "text", text: "Recover failed reasoning" }] }, + { role: "user", content: [{ type: "text", text: PARTIAL_FAILURE_CONTINUATION }] }, + ]) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user" }, + { + type: "assistant", + finish: "error", + error: { type: "provider.internal" }, + content: [{ type: "reasoning", text: "" }], + }, + { type: "synthetic", text: PARTIAL_FAILURE_CONTINUATION }, + { type: "assistant", finish: "stop", content: [{ type: "text", text: "Recovered" }] }, + ]) + expect(yield* recordedEventTypes(sessionID)).toContain("session.retry.scheduled.1") + }), + ) + + it.effect("does not continue a provider failure after tool activity", () => + Effect.gen(function* () { + const session = yield* setup + const failure = providerInternal() + yield* TestLLM.push( + TestLLM.failAfter( + failure, + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-before-provider-failure", name: "echo", input: { text: "settled" } }), + ), + ) + + expect(yield* runPrompt(session, "Do not repeat tool activity").pipe(Effect.flip)).toBe(failure) + expect(requests).toHaveLength(1) + expect(executions).toEqual(["settled"]) + expect(yield* recordedEventTypes(sessionID)).not.toContain("session.retry.scheduled.1") + }), + ) + it.effect("continues an incomplete stream after settling a local tool", () => Effect.gen(function* () { const session = yield* setup @@ -4772,7 +4845,7 @@ describe("SessionRunnerLLM", () => { content: [ { type: "text", - text: INCOMPLETE_STREAM_CONTINUATION, + text: PARTIAL_FAILURE_CONTINUATION, }, ], }, @@ -4812,7 +4885,7 @@ describe("SessionRunnerLLM", () => { }, ], }, - { type: "synthetic", text: INCOMPLETE_STREAM_CONTINUATION }, + { type: "synthetic", text: PARTIAL_FAILURE_CONTINUATION }, { type: "assistant", finish: "stop", content: [{ type: "text", text: "Recovered" }] }, ]) }), From 1b0435e464c626469ef883725c8ab0ea7df94f7b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 21 Aug 2026 19:51:17 -0400 Subject: [PATCH 2/6] fix(core): recover settled local tool failures --- packages/ai/src/protocols/openai-responses.ts | 18 +++++ .../ai/test/provider/openai-responses.test.ts | 5 ++ packages/core/src/session/runner/llm.ts | 16 +++-- packages/core/test/session-runner.test.ts | 68 ++++++++++++++++++- 4 files changed, 97 insertions(+), 10 deletions(-) diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index ee96e9abfe8d..26887472f074 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -167,6 +167,22 @@ type HostedToolItem = HostedToolData & { readonly type: HostedToolType } const isHostedToolItem = (item: OpenResponses.StreamItem): item is HostedToolItem => item.type in HOSTED_TOOLS && typeof item.id === "string" && item.id.length > 0 +const onHostedToolAdded = (state: OpenResponses.ParserState, item: HostedToolItem) => { + const events: LLMEvent[] = [] + const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + return [ + { ...state, lifecycle }, + [ + ...events, + LLMEvent.toolInputStart({ + id: item.id, + name: HOSTED_TOOLS[item.type].name, + providerExecuted: true, + }), + ], + ] satisfies OpenResponses.StepResult +} + const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item: HostedToolItem) { const isError = item.error !== undefined && item.error !== null if (item.type === "image_generation_call" && item.result) { @@ -224,6 +240,8 @@ const step = (state: OpenResponses.ParserState, event: OpenResponses.Event) => { return event.item_id ? Effect.succeed(OpenResponses.onReasoningDone(state, event)) : ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`) + if (event.type === "response.output_item.added" && event.item && isHostedToolItem(event.item)) + return Effect.succeed(onHostedToolAdded(state, event.item)) if (event.type === "response.output_item.done" && event.item && isHostedToolItem(event.item)) return onHostedToolDone(state, event.item) return OpenResponses.step(state, event) diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 82402437335d..d16f51b094c0 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -2578,6 +2578,11 @@ describe("OpenAI Responses route", () => { const callsAndResults = response.events.filter( (event) => event.type === "tool-call" || event.type === "tool-result", ) + expect(response.events.find(LLMEvent.is.toolInputStart)).toMatchObject({ + id: "ws_1", + name: "web_search", + providerExecuted: true, + }) expect(callsAndResults).toEqual([ { type: "tool-call", diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 8fd6106c9f96..440cd3c0fa6e 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -62,13 +62,15 @@ const isDecline = ( error._tag === "Permission.DeclinedError" || error._tag === "QuestionTool.CancelledError" const shouldContinueAfterFailure = (failure: AIError, record: StepRecord) => { - if (failure.reason._tag === "InvalidProviderOutput") return failure.reason.classification === "incomplete-stream" - if (failure.reason._tag === "Transport") return failure.reason.operation === "read" - // A fresh generation can continue partial output, but not cross a tool boundary - // whose provider-side effects may be ambiguous. - if (failure.reason._tag === "ProviderInternal") - return SessionRunnerRetry.isRetryable(failure) && record.calls.length === 0 - return false + const interrupted = + (failure.reason._tag === "InvalidProviderOutput" && failure.reason.classification === "incomplete-stream") || + (failure.reason._tag === "Transport" && failure.reason.operation === "read") + const providerRetry = + (failure.reason._tag === "RateLimit" || failure.reason._tag === "ProviderInternal") && + SessionRunnerRetry.isRetryable(failure) + // Settlement gives eager local executions durable outcomes. Hosted activity remains + // provider-side and is not uniformly replayable, so it cannot cross this boundary. + return (interrupted || providerRetry) && record.calls.every((call) => call.settled && !call.providerExecuted) } /** diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index d1a5548a8d80..cfb97f1a87dc 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -4795,9 +4795,10 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("does not continue a provider failure after tool activity", () => + it.effect("continues a provider failure after a settled local tool without repeating it", () => Effect.gen(function* () { const session = yield* setup + yield* admit(session, "Continue without repeating tool activity") const failure = providerInternal() yield* TestLLM.push( TestLLM.failAfter( @@ -4806,14 +4807,75 @@ describe("SessionRunnerLLM", () => { LLMEvent.toolCall({ id: "call-before-provider-failure", name: "echo", input: { text: "settled" } }), ), ) + yield* TestLLM.push(TestLLM.text("Recovered", "provider-failure-tool-recovery")) - expect(yield* runPrompt(session, "Do not repeat tool activity").pipe(Effect.flip)).toBe(failure) - expect(requests).toHaveLength(1) + const run = yield* session.resume(sessionID).pipe(Effect.forkChild) + while (!(yield* recordedEventTypes(sessionID)).includes("session.retry.scheduled.1")) yield* Effect.yieldNow + yield* TestClock.adjust("2400 millis") + yield* Fiber.join(run) + + expect(requests).toHaveLength(2) expect(executions).toEqual(["settled"]) + expect(requests[1]?.messages.slice(-3)).toMatchObject([ + { + role: "assistant", + content: [{ type: "tool-call", id: "call-before-provider-failure", name: "echo" }], + }, + { role: "tool", content: [{ type: "tool-result", id: "call-before-provider-failure" }] }, + { role: "user", content: [{ type: "text", text: PARTIAL_FAILURE_CONTINUATION }] }, + ]) + expect(yield* recordedEventTypes(sessionID)).toContain("session.retry.scheduled.1") + }), + ) + + it.effect("does not continue a provider failure after hosted tool activity", () => + Effect.gen(function* () { + const session = yield* setup + const failure = providerInternal() + yield* TestLLM.push( + TestLLM.failAfter( + failure, + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolInputStart({ id: "hosted-before-provider-failure", name: "web_search", providerExecuted: true }), + ), + ) + + expect(yield* runPrompt(session, "Do not repeat hosted activity").pipe(Effect.flip)).toBe(failure) + expect(requests).toHaveLength(1) expect(yield* recordedEventTypes(sessionID)).not.toContain("session.retry.scheduled.1") }), ) + it.effect("continues a retryable rate limit after partial output", () => + Effect.gen(function* () { + const session = yield* setup + yield* admit(session, "Recover after rate limit") + yield* TestLLM.push( + TestLLM.failAfter( + rateLimited(), + LLMEvent.stepStart({ index: 0 }), + LLMEvent.textStart({ id: "partial-rate-limit" }), + LLMEvent.textDelta({ id: "partial-rate-limit", text: "Partial" }), + ), + ) + yield* TestLLM.push(TestLLM.text(" continuation", "rate-limit-recovery")) + + const run = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* TestLLM.wait(1) + yield* TestClock.adjust("2400 millis") + yield* Fiber.join(run) + + expect(requests).toHaveLength(2) + expect(yield* recordedEventTypes(sessionID)).toContain("session.retry.scheduled.1") + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user" }, + { type: "assistant", finish: "error", content: [{ type: "text", text: "Partial" }] }, + { type: "synthetic", text: PARTIAL_FAILURE_CONTINUATION }, + { type: "assistant", finish: "stop", content: [{ type: "text", text: " continuation" }] }, + ]) + }), + ) + it.effect("continues an incomplete stream after settling a local tool", () => Effect.gen(function* () { const session = yield* setup From aa2e22b62dfa1e5e3c84a8aa5ea8e1dbf04e9077 Mon Sep 17 00:00:00 2001 From: kitlangton <7587245+kitlangton@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:24:38 +0000 Subject: [PATCH 3/6] test(core): cover Gemini partial recovery --- ...ed-tool-call-followed-by-continuation.json | 34 ++++++ .../provider/gemini-recovery.recorded.test.ts | 57 +++++++++ .../test/session-provider-recovery.test.ts | 114 ++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 packages/ai/test/fixtures/recordings/gemini-recovery/accepts-an-unsigned-settled-tool-call-followed-by-continuation.json create mode 100644 packages/ai/test/provider/gemini-recovery.recorded.test.ts create mode 100644 packages/core/test/session-provider-recovery.test.ts diff --git a/packages/ai/test/fixtures/recordings/gemini-recovery/accepts-an-unsigned-settled-tool-call-followed-by-continuation.json b/packages/ai/test/fixtures/recordings/gemini-recovery/accepts-an-unsigned-settled-tool-call-followed-by-continuation.json new file mode 100644 index 000000000000..307311bbfb3b --- /dev/null +++ b/packages/ai/test/fixtures/recordings/gemini-recovery/accepts-an-unsigned-settled-tool-call-followed-by-continuation.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:gemini-recovery", + "provider:google", + "protocol:gemini", + "tool", + "recovery" + ], + "name": "gemini-recovery/accepts-an-unsigned-settled-tool-call-followed-by-continuation", + "recordedAt": "2026-08-22T01:23:18.801Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Look up the weather in Paris.\"}]},{\"role\":\"model\",\"parts\":[{\"text\":\"I should use the weather tool.\"},{\"functionCall\":{\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"sunny\"}}}]},{\"role\":\"user\",\"parts\":[{\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}],\"toolConfig\":{\"functionCallingConfig\":{\"mode\":\"NONE\"}},\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"The weather in Paris is currently sunny\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 116,\"candidatesTokenCount\": 7,\"totalTokenCount\": 368,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 116}],\"thoughtsTokenCount\": 245,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"BfqIaun_Gbau39IP49TwKA\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"EuEKCt4KARFNMg+TrcXumi0KeXb7iZG/dBk9QjybBSM9V7hTN2a0lEXMuWh38tNhXrQCwNFDZ2gKEpa2c1lj/+wwzJAvjceCFoRenOGfBEUGRavc6oQXLKiWZPGaHV0G4gIo2uosx8SNZc4E5Dko2IYPymcUzDgsqZK2eToDoepaM8cekix+6c/z2MZiRnJ0QbS3f/7r+UYLOIi7eWbW0G6v+gwPYae4kRuifL6iKEV5eG+R4AGlGfOI0dgamDfEFfGiwg+rkvjKw7DDmYvQVreUcjvFt2OOhKfFWmnvOQrIKkSYpSyvdlylRhKZMU5gwUsKii8xwI5eJVoUoYDoOjr1x8DR3ePkCj/n3b0OMUWunPkdpgE/bUcFMQ+Gzqs7F3sBYDgUiT3F9r5joKZp6dm0I64/gh15J0NCamlr+CRLP+a8tJbEcWZbgELhbFE+2ikJMtoApojvlUbIUsn7sXHQ6042A/MjWgV97DWcG6V1dtLOOrS2Ez2F58TWDW67dTD3o+Ixz82+xDfG4VW0pOiOPOo9n2YCGk9xlSwPG9qDwU6bpi0AFrJVSz2gc8OFXk5uwCooPAN0EmnfljWS5+spS7AmKA2k3S7IZKe0aAw90+Jp4JxklOsfL4XIlQJLLXKmtcnTEdZLYp/JxSL9YRNnoHZJaeBpIWEObE+aX+CuI3OqZZPpldalNeCPM1nCYu26rfBzAaP85zzyi4SFzlXc++E9OPei8BgPoD8XuTb1yK1nTtPqjR8bYvaDm9ef8NaWrYdM+ivVQ/Q/9YAQ8AXuC71dQZExcTQAC5oOnrRP1byo2lHMuytnVk8QrnZBhiJc/TneDFzCYK9nFkhc6s0aaQrBip6YEKPtGmuMmUxJ1dQYFQlgtjEvANB2/jnPtE6u1p039reevLAHMxtijgCqNE0hrVaciI1ZTLTYtO1UquoSXNoHqqZzYuEnLMUKxTbWzIOcVBC4XwP8rkYLwCp9e1oMMBV7FIT6Kv2El7tWrpBt2w+2GgQMt2bAr2PJQf1oj5O/vOXEbcTVumOv29rUJQ65pFpr8K6Q8W6lIbIxOGWLKb/akVl9FJq5lPVp+m3eEYurQcQJ1mB3OEuPrpK4COdiVElarAnaxaMpioa8yCh22kA4eqS+bChUDIZIfCUNRDGI0eO8YMkRo626VLp9IJnexYNBBwBKk5oL7TmrlzRz/YyUEApVNMUjHiRd24i2jq4pQyOdkbYMVi3YFZNQbRuo0EN9dM8vAG4VrAGPj892T2ne5B69Qmhgs8Ow4QrubWd+d6ohdsSyOucVruw1Mr+x79BqiQs+SQ5M/GJCPBYO3QIV3CMGnHNrtvg5YdYE5blCs0MCf+u5mHPPk2TlBuXMJtLUN/YmGhE4u1/ZT9WTIueekSSubPv1eqPwLRHLiwVfKdA/FNqu5YDifgzKMjyIQpXiEIW3OnI4A1ps8K4pMynZ6XXyZuvq74eHnX1vQDdDA27MiWJPrDWqzg5SQnOl026Mcpj0pIex1lN+7av8xAc9PVszdn9qq8WwO7yUOPM10HnE2hY4pDGa3LXo/2VMAbknQOwhEA6vEgGwFVXN2EXYS9fohRgrXbo7hCroJlekcuROXX7sq2JEK751xXWZlaIpujUpNt8uUGRjSIIFKhuYYvrmU7FEN/PgB0eyQ6KQp1iQm7ywzqMF0Ns2XbM6iAlMibLu48G6VplgUWpsNMtbJLLs3QuFeligNdARJ8aMfJarHlRFV/oQXWggnSyTO5wKCOif8IJUWuHR43JStLvFZsOzVvCDduPSdWKs/9vkri3iVbRFCmeTyuJB+xQuz3rbU+iVI9VOe2BIAhKY\"}],\"role\": \"model\"},\"finishReason\": \"MAX_TOKENS\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 116,\"candidatesTokenCount\": 7,\"totalTokenCount\": 368,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 116}],\"thoughtsTokenCount\": 245,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"BfqIaun_Gbau39IP49TwKA\"}\r\n\r\n" + } + } + ] +} diff --git a/packages/ai/test/provider/gemini-recovery.recorded.test.ts b/packages/ai/test/provider/gemini-recovery.recorded.test.ts new file mode 100644 index 000000000000..8dd56209db03 --- /dev/null +++ b/packages/ai/test/provider/gemini-recovery.recorded.test.ts @@ -0,0 +1,57 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM, Message, ToolCallPart, ToolDefinition } from "../../src/index.js" +import { configure } from "../../src/providers/google.js" +import { LLMClient } from "../../src/route.js" +import { recordedTests } from "../recorded-test.js" + +const model = configure({ + apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "fixture", +}).model("gemini-3.5-flash") +const continuation = + "The previous response was interrupted. Continue from where you left off without repeating completed content." +const lookup = ToolDefinition.make({ + name: "lookup_weather", + description: "Return benign weather data for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, +}) +const recorded = recordedTests({ + prefix: "gemini-recovery", + provider: "google", + protocol: "gemini", + requires: ["GOOGLE_GENERATIVE_AI_API_KEY"], +}) + +describe("Gemini interrupted recovery recorded", () => { + recorded.effect.with( + "accepts an unsigned settled tool call followed by continuation", + { tags: ["tool", "recovery"] }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_gemini_interrupted_tool_recovery", + model, + messages: [ + Message.user("Look up the weather in Paris."), + Message.assistant([ + { type: "text", text: "I should use the weather tool." }, + ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" }), + Message.user(continuation), + ], + tools: [lookup], + toolChoice: "none", + generation: { maxTokens: 256, temperature: 0 }, + }), + ) + + expect(response.text.trim().length).toBeGreaterThan(0) + }), + ) +}) diff --git a/packages/core/test/session-provider-recovery.test.ts b/packages/core/test/session-provider-recovery.test.ts new file mode 100644 index 000000000000..b029872f36ac --- /dev/null +++ b/packages/core/test/session-provider-recovery.test.ts @@ -0,0 +1,114 @@ +import { expect, test } from "bun:test" +import { LLM, Message } from "@opencode-ai/ai" +import { configure } from "@opencode-ai/ai/providers/google" +import { compileRequest } from "@opencode-ai/ai/route/client" +import { Agent } from "@opencode-ai/core/agent" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" +import { DateTime, Effect } from "effect" + +const created = DateTime.makeUnsafe(0) +const google = Model.Ref.make({ + id: Model.ID.make("gemini-3.5-flash"), + providerID: Provider.ID.make("google"), +}) +const continuation = + "The previous response was interrupted. Continue from where you left off without repeating completed content." + +test("failed Gemini tool history replays through the public request boundary", async () => { + const messages = toLLMMessages( + [ + SessionMessage.User.make({ + id: SessionMessage.ID.make("msg_user"), + type: "user", + text: "Look up the weather.", + time: { created }, + }), + SessionMessage.Assistant.make({ + id: SessionMessage.ID.make("msg_failed"), + type: "assistant", + agent: Agent.defaultID, + model: google, + content: [ + SessionMessage.AssistantReasoning.make({ + type: "reasoning", + text: "I should use the weather tool.", + state: { thoughtSignature: "signed-reasoning" }, + }), + SessionMessage.AssistantTool.make({ + type: "tool", + id: "call_weather", + name: "lookup_weather", + executed: false, + providerState: { thoughtSignature: "signed-tool-call" }, + state: SessionMessage.ToolStateCompleted.make({ + status: "completed", + input: { city: "Paris" }, + content: [{ type: "text", text: "sunny" }], + }), + time: { created, completed: created }, + }), + ], + finish: "error", + error: { type: "provider.internal", message: "Provider stream failed" }, + time: { created, completed: created }, + }), + SessionMessage.Synthetic.make({ + id: SessionMessage.ID.make("msg_continue"), + type: "synthetic", + text: continuation, + time: { created }, + }), + ], + google, + ) + const prepared = await Effect.runPromise( + compileRequest( + LLM.request({ + model: configure({ apiKey: "test" }).model("gemini-3.5-flash"), + messages, + }), + ), + ) + + expect(messages).toMatchObject([ + { role: "user" }, + { + role: "assistant", + content: [ + { type: "text", text: "I should use the weather tool." }, + { type: "tool-call", id: "call_weather", providerMetadata: undefined }, + ], + }, + { role: "tool", content: [{ type: "tool-result", id: "call_weather" }] }, + { role: "user", content: [{ type: "text", text: continuation }] }, + ]) + expect(prepared.body.contents).toEqual([ + { role: "user", parts: [{ text: "Look up the weather." }] }, + { + role: "model", + parts: [ + { text: "I should use the weather tool.", thoughtSignature: undefined }, + { + functionCall: { id: undefined, name: "lookup_weather", args: { city: "Paris" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, + { + role: "user", + parts: [ + { + functionResponse: { + id: undefined, + name: "lookup_weather", + response: { name: "lookup_weather", content: "sunny" }, + }, + }, + ], + }, + { role: "user", parts: [{ text: continuation }] }, + ]) +}) From d44b793a911c2482424c95084e4441c048cbb46b Mon Sep 17 00:00:00 2001 From: kitlangton <7587245+kitlangton@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:52:57 +0000 Subject: [PATCH 4/6] test(core): cover Anthropic partial recovery --- ...ed-tool-call-followed-by-continuation.json | 35 +++++++ .../anthropic-recovery.recorded.test.ts | 59 +++++++++++ .../test/session-anthropic-recovery.test.ts | 99 +++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 packages/ai/test/fixtures/recordings/anthropic-recovery/accepts-a-synthetic-settled-tool-call-followed-by-continuation.json create mode 100644 packages/ai/test/provider/anthropic-recovery.recorded.test.ts create mode 100644 packages/core/test/session-anthropic-recovery.test.ts diff --git a/packages/ai/test/fixtures/recordings/anthropic-recovery/accepts-a-synthetic-settled-tool-call-followed-by-continuation.json b/packages/ai/test/fixtures/recordings/anthropic-recovery/accepts-a-synthetic-settled-tool-call-followed-by-continuation.json new file mode 100644 index 000000000000..fac0cc4cfe4a --- /dev/null +++ b/packages/ai/test/fixtures/recordings/anthropic-recovery/accepts-a-synthetic-settled-tool-call-followed-by-continuation.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:anthropic-recovery", + "provider:anthropic", + "protocol:anthropic-messages", + "tool", + "recovery" + ], + "name": "anthropic-recovery/accepts-a-synthetic-settled-tool-call-followed-by-continuation", + "recordedAt": "2026-08-22T01:52:23.916Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://opencode.ai/zen/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the weather in Paris.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I should use the weather tool.\"},{\"type\":\"tool_use\",\"id\":\"call_weather\",\"name\":\"lookup_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_weather\",\"content\":\"sunny\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"cache_control\":{\"type\":\"ephemeral\"}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":128,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_011CeGx6WkAQP5ULyRRbRwRw\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":664,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" weather in Paris is currently **sunny**! It looks like a great day to enjoy the city. ☀️\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":664,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":28} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\nevent: ping\ndata: {\"type\":\"ping\",\"cost\":\"0\"}\n\n" + } + } + ] +} diff --git a/packages/ai/test/provider/anthropic-recovery.recorded.test.ts b/packages/ai/test/provider/anthropic-recovery.recorded.test.ts new file mode 100644 index 000000000000..d9ba1ceebf9d --- /dev/null +++ b/packages/ai/test/provider/anthropic-recovery.recorded.test.ts @@ -0,0 +1,59 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM, Message, ToolCallPart, ToolDefinition } from "../../src/index.js" +import { configure } from "../../src/providers/anthropic.js" +import { LLMClient } from "../../src/route.js" +import { recordedTests } from "../recorded-test.js" + +const model = configure({ + apiKey: process.env.ZEN_TEST_API_KEY ?? "fixture", + baseURL: "https://opencode.ai/zen/v1", +}).model("claude-sonnet-4-6") +const continuation = + "The previous response was interrupted. Continue from where you left off without repeating completed content." +const lookup = ToolDefinition.make({ + name: "lookup_weather", + description: "Return benign weather data for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, +}) +const recorded = recordedTests({ + prefix: "anthropic-recovery", + provider: "anthropic", + protocol: "anthropic-messages", + requires: ["ZEN_TEST_API_KEY"], + options: { redact: { allowRequestHeaders: ["anthropic-version"] } }, +}) + +describe("Anthropic interrupted recovery recorded", () => { + recorded.effect.with( + "accepts a synthetic settled tool call followed by continuation", + { tags: ["tool", "recovery"] }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_anthropic_interrupted_tool_recovery", + model, + messages: [ + Message.user("Look up the weather in Paris."), + Message.assistant([ + { type: "text", text: "I should use the weather tool." }, + ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" }), + Message.user(continuation), + ], + tools: [lookup], + toolChoice: "none", + generation: { maxTokens: 128, temperature: 0 }, + }), + ) + + expect(response.text.trim().length).toBeGreaterThan(0) + }), + ) +}) diff --git a/packages/core/test/session-anthropic-recovery.test.ts b/packages/core/test/session-anthropic-recovery.test.ts new file mode 100644 index 000000000000..347c0f168dbb --- /dev/null +++ b/packages/core/test/session-anthropic-recovery.test.ts @@ -0,0 +1,99 @@ +import { expect, test } from "bun:test" +import { LLM } from "@opencode-ai/ai" +import { configure } from "@opencode-ai/ai/providers/anthropic" +import { compileRequest } from "@opencode-ai/ai/route/client" +import { Agent } from "@opencode-ai/core/agent" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" +import { DateTime, Effect } from "effect" + +const created = DateTime.makeUnsafe(0) +const anthropic = Model.Ref.make({ + id: Model.ID.make("claude-sonnet-4-6"), + providerID: Provider.ID.make("anthropic"), +}) +const continuation = + "The previous response was interrupted. Continue from where you left off without repeating completed content." + +test("failed Anthropic tool history replays through the public request boundary", async () => { + const messages = toLLMMessages( + [ + SessionMessage.User.make({ + id: SessionMessage.ID.make("msg_user"), + type: "user", + text: "Look up the weather.", + time: { created }, + }), + SessionMessage.Assistant.make({ + id: SessionMessage.ID.make("msg_failed"), + type: "assistant", + agent: Agent.defaultID, + model: anthropic, + content: [ + SessionMessage.AssistantReasoning.make({ + type: "reasoning", + text: "I should use the weather tool.", + state: { signature: "signed-reasoning" }, + }), + SessionMessage.AssistantTool.make({ + type: "tool", + id: "call_weather", + name: "lookup_weather", + executed: false, + state: SessionMessage.ToolStateCompleted.make({ + status: "completed", + input: { city: "Paris" }, + content: [{ type: "text", text: "sunny" }], + }), + time: { created, completed: created }, + }), + ], + finish: "error", + error: { type: "provider.internal", message: "Provider stream failed" }, + time: { created, completed: created }, + }), + SessionMessage.Synthetic.make({ + id: SessionMessage.ID.make("msg_continue"), + type: "synthetic", + text: continuation, + time: { created }, + }), + ], + anthropic, + ) + const prepared = await Effect.runPromise( + compileRequest( + LLM.request({ + model: configure({ apiKey: "test" }).model("claude-sonnet-4-6"), + messages, + }), + ), + ) + + expect(messages).toMatchObject([ + { role: "user" }, + { + role: "assistant", + content: [ + { type: "text", text: "I should use the weather tool." }, + { type: "tool-call", id: "call_weather", providerMetadata: undefined }, + ], + }, + { role: "tool", content: [{ type: "tool-result", id: "call_weather" }] }, + { role: "user", content: [{ type: "text", text: continuation }] }, + ]) + expect(prepared.body.messages).toMatchObject([ + { role: "user", content: [{ type: "text", text: "Look up the weather." }] }, + { + role: "assistant", + content: [ + { type: "text", text: "I should use the weather tool." }, + { type: "tool_use", id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }, + ], + }, + { role: "user", content: [{ type: "tool_result", tool_use_id: "call_weather", content: "sunny" }] }, + { role: "user", content: [{ type: "text", text: continuation }] }, + ]) +}) From 361a5708022365e2e3de5c23b4f9722c757fcc34 Mon Sep 17 00:00:00 2001 From: kitlangton <7587245+kitlangton@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:18:25 +0000 Subject: [PATCH 5/6] test(ai): cover parallel recovery replay --- ...rallel-tools-followed-by-continuation.json | 36 +++++++++++++++++ ...rallel-tools-followed-by-continuation.json | 35 +++++++++++++++++ .../anthropic-recovery.recorded.test.ts | 39 +++++++++++++++++++ .../provider/gemini-recovery.recorded.test.ts | 39 +++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 packages/ai/test/fixtures/recordings/anthropic-recovery/accepts-synthetic-settled-parallel-tools-followed-by-continuation.json create mode 100644 packages/ai/test/fixtures/recordings/gemini-recovery/accepts-unsigned-settled-parallel-tools-followed-by-continuation.json diff --git a/packages/ai/test/fixtures/recordings/anthropic-recovery/accepts-synthetic-settled-parallel-tools-followed-by-continuation.json b/packages/ai/test/fixtures/recordings/anthropic-recovery/accepts-synthetic-settled-parallel-tools-followed-by-continuation.json new file mode 100644 index 000000000000..b0dbb089a959 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/anthropic-recovery/accepts-synthetic-settled-parallel-tools-followed-by-continuation.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:anthropic-recovery", + "provider:anthropic", + "protocol:anthropic-messages", + "tool", + "parallel", + "recovery" + ], + "name": "anthropic-recovery/accepts-synthetic-settled-parallel-tools-followed-by-continuation", + "recordedAt": "2026-08-22T02:18:18.865Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://opencode.ai/zen/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the weather and local time in Paris.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I should use both lookup tools.\"},{\"type\":\"tool_use\",\"id\":\"call_weather\",\"name\":\"lookup_weather\",\"input\":{\"city\":\"Paris\"}},{\"type\":\"tool_use\",\"id\":\"call_time\",\"name\":\"lookup_time\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_weather\",\"content\":\"sunny\"},{\"type\":\"tool_result\",\"tool_use_id\":\"call_time\",\"content\":\"12:00\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}},{\"name\":\"lookup_time\",\"description\":\"Return the benign local time for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"cache_control\":{\"type\":\"ephemeral\"}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":128,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_011CeGz56HhX4x8LT1HGmLfn\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":814,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Here\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" are\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" the results for\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" Paris:\\n\\n-\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" **Weather:**\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" It\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" is currently sunny in Paris.\\n- **Local Time:** The current local time in Paris is 12:00 PM (noon).\\n\\nLet me know if you need any other information!\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":814,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":55} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\nevent: ping\ndata: {\"type\":\"ping\",\"cost\":\"0\"}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/gemini-recovery/accepts-unsigned-settled-parallel-tools-followed-by-continuation.json b/packages/ai/test/fixtures/recordings/gemini-recovery/accepts-unsigned-settled-parallel-tools-followed-by-continuation.json new file mode 100644 index 000000000000..1c9140c76c90 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/gemini-recovery/accepts-unsigned-settled-parallel-tools-followed-by-continuation.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:gemini-recovery", + "provider:google", + "protocol:gemini", + "tool", + "parallel", + "recovery" + ], + "name": "gemini-recovery/accepts-unsigned-settled-parallel-tools-followed-by-continuation", + "recordedAt": "2026-08-22T02:18:17.994Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Look up the weather and local time in Paris.\"}]},{\"role\":\"model\",\"parts\":[{\"text\":\"I should use both lookup tools.\"},{\"functionCall\":{\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"},{\"functionCall\":{\"name\":\"lookup_time\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"sunny\"}}},{\"functionResponse\":{\"name\":\"lookup_time\",\"response\":{\"name\":\"lookup_time\",\"content\":\"12:00\"}}}]},{\"role\":\"user\",\"parts\":[{\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}},{\"name\":\"lookup_time\",\"description\":\"Return the benign local time for a city.\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}],\"toolConfig\":{\"functionCallingConfig\":{\"mode\":\"NONE\"}},\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"In Paris, the weather is currently sunny, and the local time is 12:00.\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 205,\"candidatesTokenCount\": 21,\"totalTokenCount\": 420,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 205}],\"thoughtsTokenCount\": 194,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"6AaJaoOsJrbI-8YPpqnz0Qg\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"Eq0ICqoIARFNMg8GCNXth8PULrLVoOm0IMGKXbEMpyUD0xr35HHiDNk+Hzd0ZcnORGZeHhr2X4aUfGEBNHLfiPZB58GEozrIKnaij2tY39bV2kwdmauBdATebzhry32Tg0eo8byo1pMdIUQyY8lNoXo5X3zGWjzwiODhG4vOMHAYczSBpj0wuWgQk4GtT9dLi10jNSML4Sjh4ObUSDZYCoLjT7gk8iOcE9imM7O0b6MmHX0oi9gN4n/MF6JZQsmbtHA81zISYq6ADroKmyBdr36pETBj0Z36vRkJYcw+UqMgmTUQ4X0qwCYT+rPd2m6OSgI1mk1ApGr3DKV7ovQGztfbSg1r8mrNIkfbgmptl0vrdWSNonXugIdftdkWA5DvN5HBJlQzutJ9ggYDN8n6Jila1vOwwIPojTKfqOLOrR9gYFSNMe8lVBMuVIsy/ogmVYOkMLb0yUAIQvR5+99ZVmSTiys2GMgf6Q7VYckZIaXXemdHBM/LPXAc6TeYKLODEbjfoyxmmX8VUEnjv1fRJPb9tf7ii7/Js6rsndgQu2UZIC+b/ui8ayCHs+8KCMw4QGijxrdhRM1yo6RRbsEkQxnm0HPyC3I5l1VmZO8WFnn4nBBJ0O6UWBsvy5/KvkgbKj612f6TG7RveK3QNHHGKC2Gd1ztIBOwzB32/x/zvK8X1T950OtiF8a0ExrEI8D9UCbMyFJBDiK8EI5SadKNUTCvdx7P1zwAqjKhOQ8SWF3Fs5xDH1Ea4qRWlUXKzddrj0e9BLDFuUUQ9BSuekbpgTCdm/ArllXKJt2mVrPi1iLFFhDf1rnHSbTamN6aBrT8mjzdu3w5/KI62marHZmHwgQKm89Pw/1ZzZYUjISEAX3SyrMVUW4K4I1goPEjdrY+VcOlRNuz8geZAasM9TkgjxZuHdFQoGMmulLjoBhwFii5jWq4DrC10mfXhGFYS0SNncOj3ZUmwfkEQ5KslsKoNNicHb9TTElKwUcpcAFSLQ2WFG60qd/kXR4EBriIaD5B8cAG/oVjjd0+PRNRrdSV+FMz/N2fZrP+MH7XiH5i+jjhk0/YDgwuAINYrUdLmmMP+IT4MALPhH1SlubyUxE1uUO4+ayutIWMWX5a9zGwSWWK9ehb3cdMUtZwACqlwlCnUIOoVMGcgEFZNcWQv7nhey51U2713L4Ud53s797TeUQF6GG0FssnaBP8ekF0Xo94H4jHUaLmgW2n6OQfOeut/6h1qqRNEocVVFPAKdv0RO3DyC0Ng0rCV98+v6Qp4fK+tIynZobBzoK3h/Bw3/kWAXAgbu4m71/yIe5rQRT9Y3aaJrsJU0TbtSORagcxUYZ4Shpap8RBLGnnN0ozriEsU5ySXyZUK7IwJdO33gfE1bBFhDXITqD8JE32pb+KhaFJ84gjsicsamJBMSVkl+RjPw==\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 205,\"candidatesTokenCount\": 21,\"totalTokenCount\": 420,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 205}],\"thoughtsTokenCount\": 194,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"6AaJaoOsJrbI-8YPpqnz0Qg\"}\r\n\r\n" + } + } + ] +} diff --git a/packages/ai/test/provider/anthropic-recovery.recorded.test.ts b/packages/ai/test/provider/anthropic-recovery.recorded.test.ts index d9ba1ceebf9d..2af31b968bc6 100644 --- a/packages/ai/test/provider/anthropic-recovery.recorded.test.ts +++ b/packages/ai/test/provider/anthropic-recovery.recorded.test.ts @@ -20,6 +20,15 @@ const lookup = ToolDefinition.make({ required: ["city"], }, }) +const clock = ToolDefinition.make({ + name: "lookup_time", + description: "Return the benign local time for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, +}) const recorded = recordedTests({ prefix: "anthropic-recovery", provider: "anthropic", @@ -56,4 +65,34 @@ describe("Anthropic interrupted recovery recorded", () => { expect(response.text.trim().length).toBeGreaterThan(0) }), ) + + recorded.effect.with( + "accepts synthetic settled parallel tools followed by continuation", + { tags: ["tool", "parallel", "recovery"] }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_anthropic_interrupted_parallel_recovery", + model, + messages: [ + Message.user("Look up the weather and local time in Paris."), + Message.assistant([ + { type: "text", text: "I should use both lookup tools." }, + ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }), + ToolCallPart.make({ id: "call_time", name: "lookup_time", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" }), + Message.tool({ id: "call_time", name: "lookup_time", result: "12:00", resultType: "text" }), + Message.user(continuation), + ], + tools: [lookup, clock], + toolChoice: "none", + generation: { maxTokens: 128, temperature: 0 }, + }), + ) + + expect(response.text.trim().length).toBeGreaterThan(0) + }), + ) }) diff --git a/packages/ai/test/provider/gemini-recovery.recorded.test.ts b/packages/ai/test/provider/gemini-recovery.recorded.test.ts index 8dd56209db03..2b2b6d183bce 100644 --- a/packages/ai/test/provider/gemini-recovery.recorded.test.ts +++ b/packages/ai/test/provider/gemini-recovery.recorded.test.ts @@ -19,6 +19,15 @@ const lookup = ToolDefinition.make({ required: ["city"], }, }) +const clock = ToolDefinition.make({ + name: "lookup_time", + description: "Return the benign local time for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, +}) const recorded = recordedTests({ prefix: "gemini-recovery", provider: "google", @@ -54,4 +63,34 @@ describe("Gemini interrupted recovery recorded", () => { expect(response.text.trim().length).toBeGreaterThan(0) }), ) + + recorded.effect.with( + "accepts unsigned settled parallel tools followed by continuation", + { tags: ["tool", "parallel", "recovery"] }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_gemini_interrupted_parallel_recovery", + model, + messages: [ + Message.user("Look up the weather and local time in Paris."), + Message.assistant([ + { type: "text", text: "I should use both lookup tools." }, + ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }), + ToolCallPart.make({ id: "call_time", name: "lookup_time", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" }), + Message.tool({ id: "call_time", name: "lookup_time", result: "12:00", resultType: "text" }), + Message.user(continuation), + ], + tools: [lookup, clock], + toolChoice: "none", + generation: { maxTokens: 256, temperature: 0 }, + }), + ) + + expect(response.text.trim().length).toBeGreaterThan(0) + }), + ) }) From 24174698edc5a31cd263a1d7e90439078a226c94 Mon Sep 17 00:00:00 2001 From: kitlangton <7587245+kitlangton@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:23:31 +0000 Subject: [PATCH 6/6] test(ai): record provider recovery matrix --- ...rallel-tools-followed-by-continuation.json | 37 ++++++++ ...n-with-a-missing-parallel-tool-result.json | 37 ++++++++ ...settled-tool-followed-by-continuation.json | 34 +++++++ ...-continuation-with-an-unresolved-tool.json | 35 +++++++ ...rallel-tools-followed-by-continuation.json | 35 +++++++ ...n-with-a-missing-parallel-tool-result.json | 36 +++++++ ...rallel-tools-followed-by-continuation.json | 35 +++++++ .../ai/test/provider/bedrock-converse.test.ts | 4 +- .../bedrock-recovery.recorded.test.ts | 94 +++++++++++++++++++ .../provider/meta-recovery.recorded.test.ts | 76 +++++++++++++++ .../provider/openai-recovery.recorded.test.ts | 92 ++++++++++++++++++ .../provider/xai-recovery.recorded.test.ts | 68 ++++++++++++++ 12 files changed, 582 insertions(+), 1 deletion(-) create mode 100644 packages/ai/test/fixtures/recordings/bedrock-recovery/accepts-settled-parallel-tools-followed-by-continuation.json create mode 100644 packages/ai/test/fixtures/recordings/bedrock-recovery/rejects-continuation-with-a-missing-parallel-tool-result.json create mode 100644 packages/ai/test/fixtures/recordings/meta-recovery/accepts-a-settled-tool-followed-by-continuation.json create mode 100644 packages/ai/test/fixtures/recordings/meta-recovery/rejects-continuation-with-an-unresolved-tool.json create mode 100644 packages/ai/test/fixtures/recordings/openai-recovery/accepts-settled-parallel-tools-followed-by-continuation.json create mode 100644 packages/ai/test/fixtures/recordings/openai-recovery/rejects-continuation-with-a-missing-parallel-tool-result.json create mode 100644 packages/ai/test/fixtures/recordings/xai-recovery/accepts-settled-parallel-tools-followed-by-continuation.json create mode 100644 packages/ai/test/provider/bedrock-recovery.recorded.test.ts create mode 100644 packages/ai/test/provider/meta-recovery.recorded.test.ts create mode 100644 packages/ai/test/provider/openai-recovery.recorded.test.ts create mode 100644 packages/ai/test/provider/xai-recovery.recorded.test.ts diff --git a/packages/ai/test/fixtures/recordings/bedrock-recovery/accepts-settled-parallel-tools-followed-by-continuation.json b/packages/ai/test/fixtures/recordings/bedrock-recovery/accepts-settled-parallel-tools-followed-by-continuation.json new file mode 100644 index 000000000000..245fbb3f84b2 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/bedrock-recovery/accepts-settled-parallel-tools-followed-by-continuation.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:bedrock-recovery", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "tool", + "parallel", + "recovery", + "bearer" + ], + "name": "bedrock-recovery/accepts-settled-parallel-tools-followed-by-continuation", + "recordedAt": "2026-08-22T02:23:15.022Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-2.amazonaws.com/model/us.anthropic.claude-sonnet-4-6/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"Look up the weather and local time in Paris.\"}]},{\"role\":\"assistant\",\"content\":[{\"text\":\"I should use both lookup tools.\"},{\"toolUse\":{\"toolUseId\":\"call_weather\",\"name\":\"lookup_weather\",\"input\":{\"city\":\"Paris\"}}},{\"toolUse\":{\"toolUseId\":\"call_time\",\"name\":\"lookup_time\",\"input\":{\"city\":\"Paris\"}}}]},{\"role\":\"user\",\"content\":[{\"toolResult\":{\"toolUseId\":\"call_weather\",\"content\":[{\"text\":\"sunny\"}],\"status\":\"success\"}},{\"toolResult\":{\"toolUseId\":\"call_time\",\"content\":[{\"text\":\"12:00\"}],\"status\":\"success\"}},{\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"},{\"cachePoint\":{\"type\":\"default\"}}]}],\"inferenceConfig\":{\"maxTokens\":128,\"temperature\":0},\"toolConfig\":{\"tools\":[{\"toolSpec\":{\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"inputSchema\":{\"json\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}}},{\"toolSpec\":{\"name\":\"lookup_time\",\"description\":\"Return the benign local time for a city.\",\"inputSchema\":{\"json\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}}},{\"cachePoint\":{\"type\":\"default\"}}]}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAAfwAAAFKbxZivCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiIiwicm9sZSI6ImFzc2lzdGFudCJ9NixsGAAAAMYAAABX9ngGRQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJIZXJlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OTyJ9FU7dogAAAL0AAABXeHrXmws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgYXJlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRiJ91dsTawAAANsAAABXbghVdgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgdGhlIHJlc3VsdHMgZm9yIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWCJ9/TLV9AAAAOEAAABXhZn1UQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgUGFyaXM6XG5cbi0ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2NyJ91HwjXQAAAKwAAABXJfppqQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgKipXZWF0aGVyOioqIn0sInAiOiJhYmNkZWYifQYy8SgAAACrAAAAV5fatbkLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIEl0In0sInAiOiJhYmNkZWZnaGlqa2xtbm8ifYhwCMcAAADFAAAAV7HYfJULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGlzIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OTyJ9/mrm9AAAALoAAABXyloLiws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgY3VycmVudGx5IHN1bm55IGluIFBhcmlzLiJ9LCJwIjoiYWJjZGVmZyJ9YOUnoQAAALwAAABXRRr+Kws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJcbi0gKipMb2NhbCBUaW1lOioqIFRoZSJ9LCJwIjoiYWJjZGVmZ2hpamtsIn2nYi93AAAAqwAAAFeX2rW5CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBjdXJyZW50IGxvY2FsIn0sInAiOiJhYmNkIn2AqHDZAAAA2QAAAFcUyAYWCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0aW1lIGluIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMyJ9IdIoaQAAALoAAABXyloLiws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgUGFyaXMgaXMgMTI6MDAgKCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9dVhq1AAAAM8AAABX+2hkNAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJub29uIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWCJ9lAMZFQAAAMEAAABXRFjaVQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIpLlxuXG5MZXQifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREUifdXnWuwAAADiAAAAV8I5j4ELOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIG1lIGtub3cgaWYgeW91IG5lZWQifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowIn2/jqXoAAAA6QAAAFe16b6QCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBhbnkgb3RoZXIgaW5mb3JtYXRpb24hIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQifSUreT4AAACWAAAAVrls+RgLOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXIifesj19wAAACxAAAAUVTpn68LOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVYiLCJzdG9wUmVhc29uIjoiZW5kX3R1cm4ifcT0cWcAAADzAAAATvvSmXMLOmV2ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0ZW5jeU1zIjoxNzQ3fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QSIsInVzYWdlIjp7ImlucHV0VG9rZW5zIjo4MTQsIm91dHB1dFRva2VucyI6NTQsInNlcnZlclRvb2xVc2FnZSI6e30sInRvdGFsVG9rZW5zIjo4Njh9fW/lT4s=", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/bedrock-recovery/rejects-continuation-with-a-missing-parallel-tool-result.json b/packages/ai/test/fixtures/recordings/bedrock-recovery/rejects-continuation-with-a-missing-parallel-tool-result.json new file mode 100644 index 000000000000..1e0da96af783 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/bedrock-recovery/rejects-continuation-with-a-missing-parallel-tool-result.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:bedrock-recovery", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "tool", + "parallel", + "recovery", + "bearer", + "sad-path" + ], + "name": "bedrock-recovery/rejects-continuation-with-a-missing-parallel-tool-result", + "recordedAt": "2026-08-22T02:23:15.290Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-2.amazonaws.com/model/us.anthropic.claude-sonnet-4-6/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"Look up the weather and local time in Paris.\"}]},{\"role\":\"assistant\",\"content\":[{\"text\":\"I should use both lookup tools.\"},{\"toolUse\":{\"toolUseId\":\"call_weather\",\"name\":\"lookup_weather\",\"input\":{\"city\":\"Paris\"}}},{\"toolUse\":{\"toolUseId\":\"call_time\",\"name\":\"lookup_time\",\"input\":{\"city\":\"Paris\"}}}]},{\"role\":\"user\",\"content\":[{\"toolResult\":{\"toolUseId\":\"call_weather\",\"content\":[{\"text\":\"sunny\"}],\"status\":\"success\"}},{\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"},{\"cachePoint\":{\"type\":\"default\"}}]}],\"inferenceConfig\":{\"maxTokens\":32,\"temperature\":0},\"toolConfig\":{\"tools\":[{\"toolSpec\":{\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"inputSchema\":{\"json\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}}},{\"toolSpec\":{\"name\":\"lookup_time\",\"description\":\"Return the benign local time for a city.\",\"inputSchema\":{\"json\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}}},{\"cachePoint\":{\"type\":\"default\"}}]}}" + }, + "response": { + "status": 400, + "headers": { + "content-type": "application/json" + }, + "body": "{\"message\":\"Expected toolResult blocks at messages.2.content for the following Ids: call_time\"}" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/meta-recovery/accepts-a-settled-tool-followed-by-continuation.json b/packages/ai/test/fixtures/recordings/meta-recovery/accepts-a-settled-tool-followed-by-continuation.json new file mode 100644 index 000000000000..f880ade8a1ae --- /dev/null +++ b/packages/ai/test/fixtures/recordings/meta-recovery/accepts-a-settled-tool-followed-by-continuation.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:meta-recovery", + "provider:meta", + "protocol:openai-responses", + "tool", + "recovery" + ], + "name": "meta-recovery/accepts-a-settled-tool-followed-by-continuation", + "recordedAt": "2026-08-22T02:20:24.363Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.meta.ai/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"muse-spark-1.2\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Look up the weather in Paris.\"}]},{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"I should use the weather tool.\"}]},{\"type\":\"function_call\",\"call_id\":\"call_weather\",\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_weather\",\"output\":\"sunny\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"store\":false,\"max_output_tokens\":128,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "event: response.created\ndata: {\"response\":{\"background\":false,\"created_at\":1787365223.0,\"error\":null,\"id\":\"resp_6a890767037bffd1d1154065\",\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":128,\"model\":\"muse-spark-1.2\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"reasoning\":{\"effort\":\"high\"},\"service_tier\":\"auto\",\"status\":\"in_progress\",\"store\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"tools\":[{\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"parameters\":{\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"type\":\"object\"},\"strict\":false,\"type\":\"function\"}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\"},\"sequence_number\":0,\"type\":\"response.created\"}\n\nevent: response.in_progress\ndata: {\"response\":{\"background\":false,\"created_at\":1787365223.0,\"error\":null,\"id\":\"resp_6a890767037bffd1d1154065\",\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":128,\"model\":\"muse-spark-1.2\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"reasoning\":{\"effort\":\"high\"},\"service_tier\":\"auto\",\"status\":\"in_progress\",\"store\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"tools\":[{\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"parameters\":{\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"type\":\"object\"},\"strict\":false,\"type\":\"function\"}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\"},\"sequence_number\":1,\"type\":\"response.in_progress\"}\n\nevent: response.output_item.added\ndata: {\"item\":{\"id\":\"rs_6a890767037bffd1d1154065:rs_01a02744ec457db1bc51d366eb297f28\",\"status\":\"in_progress\",\"summary\":[],\"type\":\"reasoning\"},\"output_index\":0,\"sequence_number\":2,\"type\":\"response.output_item.added\"}\n\nevent: response.incomplete\ndata: {\"response\":{\"background\":false,\"created_at\":1787365223.0,\"error\":null,\"id\":\"resp_6a890767037bffd1d1154065\",\"incomplete_details\":{\"reason\":\"max_output_tokens\"},\"instructions\":null,\"max_output_tokens\":128,\"model\":\"muse-spark-1.2\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"reasoning\":{\"effort\":\"high\"},\"service_tier\":\"auto\",\"status\":\"incomplete\",\"store\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"tools\":[{\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"parameters\":{\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"type\":\"object\"},\"strict\":false,\"type\":\"function\"}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":646,\"input_tokens_details\":{\"cached_tokens\":177},\"output_tokens\":128,\"output_tokens_details\":{\"reasoning_tokens\":125},\"total_tokens\":774}},\"sequence_number\":3,\"type\":\"response.incomplete\"}\n\ndata: [DONE]\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/meta-recovery/rejects-continuation-with-an-unresolved-tool.json b/packages/ai/test/fixtures/recordings/meta-recovery/rejects-continuation-with-an-unresolved-tool.json new file mode 100644 index 000000000000..85f2edf7f8db --- /dev/null +++ b/packages/ai/test/fixtures/recordings/meta-recovery/rejects-continuation-with-an-unresolved-tool.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:meta-recovery", + "provider:meta", + "protocol:openai-responses", + "tool", + "recovery", + "sad-path" + ], + "name": "meta-recovery/rejects-continuation-with-an-unresolved-tool", + "recordedAt": "2026-08-22T02:20:24.441Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.meta.ai/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"muse-spark-1.2\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Look up the weather in Paris.\"}]},{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"I should use the weather tool.\"}]},{\"type\":\"function_call\",\"call_id\":\"call_weather\",\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"store\":false,\"max_output_tokens\":32,\"stream\":true}" + }, + "response": { + "status": 400, + "headers": { + "content-type": "application/json" + }, + "body": "{\"error\":{\"code\":null,\"message\":\"No tool output found for function call call_weather.\",\"param\":\"input\",\"type\":\"invalid_request_error\"}}" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/openai-recovery/accepts-settled-parallel-tools-followed-by-continuation.json b/packages/ai/test/fixtures/recordings/openai-recovery/accepts-settled-parallel-tools-followed-by-continuation.json new file mode 100644 index 000000000000..aad51fa2c3fc --- /dev/null +++ b/packages/ai/test/fixtures/recordings/openai-recovery/accepts-settled-parallel-tools-followed-by-continuation.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:openai-recovery", + "provider:openai", + "protocol:openai-responses", + "tool", + "parallel", + "recovery" + ], + "name": "openai-recovery/accepts-settled-parallel-tools-followed-by-continuation", + "recordedAt": "2026-08-22T02:20:25.256Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Look up the weather and local time in Paris.\"}]},{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"I should use both lookup tools.\"}]},{\"type\":\"function_call\",\"call_id\":\"call_weather\",\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call\",\"call_id\":\"call_time\",\"name\":\"lookup_time\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_weather\",\"output\":\"sunny\"},{\"type\":\"function_call_output\",\"call_id\":\"call_time\",\"output\":\"12:00\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"name\":\"lookup_time\",\"description\":\"Return the benign local time for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"tool_choice\":\"none\",\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":128,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_089fa9a63daf7149016a890767198c87d2a11df03750e42a8c\",\"object\":\"response\",\"created_at\":1787365223,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":128,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"none\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"description\":\"Return the benign local time for a city.\",\"name\":\"lookup_time\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_089fa9a63daf7149016a890767198c87d2a11df03750e42a8c\",\"object\":\"response\",\"created_at\":1787365223,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":128,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"none\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"description\":\"Return the benign local time for a city.\",\"name\":\"lookup_time\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_089fa9a63daf7149016a890767a3b887d2965e2b629f2b4fec\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqiQdn0sKqOTz_QP6iFsxzHmd0E1CVke8bs-c8Ls8ssW6Ui8xYH2okIeCRRmccKR2foeBBQSLj4Pi-C4ah28M1Y3vwAZnY2AC9If2KbESeoIQlTXipPz9xE5EEWz7lyfoXOvkJFVGAXBWR5ZpQ2p_e4gSXc0iQqbkRFsF_mDFkL_1RHhhPwiAI1f-p0FRb3ESTVh79yZjqKkFsPN7ypdL8ESoiESGYRnKrI0txGmnetW2Wsl4_PQ1qamt385ROvmbwILoC90ebRb7RTXGDldPM_Obr4CJHjOiWjZusoOJUuEK7MmViG3xi0jOvfurjrz9ix5mlQj2nyGmqwo2J63p7B3UYONQQHI9cr__PG96hpoUc48OgAPcoQisUJdbnsbn-fx_djHEv7ok7GT8YKu_skYc6Ijmnrkkeh2c0_pZ5dEfVorUSyHEEJYOj2UNWnXUjblNplpA83X_zYt7noW2lV0POqG5uDNgjU-hNFJbMmIzIeiVXZpTW2yQpnSVHf9XOmfShACtmTqbuMyGUA7Fbzh_cZBmWjJu8P5tqVkC60ywiOankAuXr3C8KJBmOV93fK3NNOBQ74r3WCNwgkr7V2j1IKIsSLsc0zFzPMGlWlagQhnD8ETl_jUNnTC8R53bWr83d4CJbM9QqTFS_XunhAl8jaRG69o9DUPdb0BXb82CM8b1dJNvrHbiQV5t0Bp6wbVpu7wPxU2nGYjqt_Da29T2N6Oua3n-T0xm7wgJwXay7bJo8c-1rdYwNc4O2NW2ckWFFjVd5HNdncRuPugl4g45_kt1MJrj7kruz2Qi0ZBH7T7hXq-_8guysG3a7riiGgkW7HXYmoWFiqsfRyQ-lfChvKqoi8rOprgtjpbC_HMd86Llz2Jw7WD_TVxvzqMIFSMSuq47rWAOXYg7nzLHA2QoEHm47GAquXyIF_934YBcfYvs0IUrqWDJlbAzPeUjgTnP7tBd3j7MRK5oLCnxPfjHD_kbAaM5Sz4t4Oe5khyCQMNrBGQ-ofrpuT49FLB_yGuzYOaHHVDC64mqmiMLDcLUkkgqiBUV2nvqJg2XJgS4M7zLuWSTI3ncyiF5kMX7dMvxsa-j8Gs_KTbjM08sTBeO0GEV27FxeDLirRhoaX94A-6Lqf6T6ZAGtD2-sB2_0zS27KIZJ01KyAZDOIvx7w4pkzw==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_089fa9a63daf7149016a890767a3b887d2965e2b629f2b4fec\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqiQdpXWZ8kVNEA6t_0i0mRaQ_NkCi8REGF14pC5LS_KpYuh60dgnXN86K12sHMr7ELMJgVHM2DUBdMUTa6mYMqxDNBhrWTvqueKjK40Hp3NBdMQz7UE29CN_zu3aaHIC9KUkcORWpDYigP60HPgzLIeC89C4v28R9O_zyfl6DrwYqFoc-KSY9_Smkz8tBNzbX4bSAfZqD_OyFmXKmF4YJKtefKL2qkg1pzqSwbdfeepWRWFCjzoy2G44H9Ii6F5ztYhkJHphVMIXnsq-tTAYVltfI8C2MVAfZrk8xfcyykG1IqctcemzPCusd7t21f3i0oi6SuaTaoM3SjV3qDPo5cHALn9qiJCXV9SYKN-3Wyr6ClyIvuvNIZ4acNwViqHRHH0pkTFHunb4SLLhyfHorwcNGqAz3S9YcDRaZErfUTZbgTDJm6qew0s0OnavD7Xov_vFD8dzu8yqa8iaYgQr_4w9IL1kJG466ea22-bN36gGRVzEbJ9GZPdzIyJ773nUPGCWytIQILxsJojAQ-EVcYOZQOCYF4HsOD73qmUtaW_izOpDNKHnhpWB5PRKL1en5Owrg3FV7EvpUYChsqHz8GHNhlzCOl6D6BU7ls14-KekozznLarkFaNrri0k7dtPq8Po2pZCV00sr0NZ649MOjMAuSNaJUaQPKPM2j5D8ljKZGguR5yD8md99dSh5cQNlRaWy2UdiRyG2WiPzOZD3wS9RDuA-Gros-1XEaGxDXy9ouK-vXLEq8r6uOGM4GNTzTrnSlpQDN6SlzVbop5B5WH_KKntOeIJ76WtupSQMwSIV1NgzlyBAhhagDF10aAw78cIUiu5SMbxAI0KFMbIWAs7E3LhSyOdNmId85Ix7Qo0-0h9jkUS1SWyeITla83iuLQyOlUK8Ho5sy8XNxtYWVRJbaV6i_0OMacp7-aQffXffOSoU8U3td4kaH47-PBhYu3rVRGpg1LuuWqWD1iku8olDFv_SPSWgkJMHazp9P8VbTkyXUKLZxUWDULOQfJ65ZjSmAuTNZCNNfpYBk04oy7ewQdIf-BNn_M_NS5km89H5T2Cah95TnQ4ebJJEdMpz1JWo4ZCqTK05IWxV4AQrIdTv6BWujhCT22qTFLblIuKxzgAndto543ZkJLkakgqx8Dvm6TSAvWaJY0Lr-TbG0sgjLn_FEyL-c5j9pzXR4sl60o486VUYyrNT5mTGpTYVuLjYaxhZd_s1sM-UZypTrB9H_SjX80QXqFguvvh4Zl31Hb8CJAsShmfCzw3lBwiH7xgnThM-aGquQ0TNMXq7f6xnfDF3VxlMT2YonK1DRKaX2kPqZPf29VpTzHFdvxQghDYvQ6KIcEkis2tqwsor3YDEm_drp7TTqNAvKNcmJ7BMx1zwjXQtXBS3e9dsaGH678Geck8PqYXjcEuvPPFN_KU1iOG9pGy5PscDUrDR2p9ZE2zSzMQlfFzWj3e8E_5YF4BG8IpidDcXA8PrPKJs0pL1tWKf43lnd9s9Ks7LHKjYD0vsVTf-UZp-avxPHrkn8Rde-eka2wvA1F_6I0seBUlR8JSp-H9S-R-EiYGled6fczWMfAEY0LZFEQ8k9WTbMw6gsIE2csCxVMGbCJmQ4-Ii0QHl720DK9xjZGgT8TVOjOMVYAbiuc0oJmA2fq0fJiJzBMbJ0hI2COmVwWxaulIWNpM6TkhlzyehnZ9QvZx0eGQ1fBFtfHQnZxKiwKo6At0ZgrRLkt8_rS_y9Er3mttZuw==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Paris\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"w72chU99vBM\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\":\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"7xNDuOsXwzA96Sa\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" sunny\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"hKkytqKUad\",\"output_index\":1,\"sequence_number\":8}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\",\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"HHtHOR1VfzzfDbO\",\"output_index\":1,\"sequence_number\":9}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" local\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"THPtPaIyzS\",\"output_index\":1,\"sequence_number\":10}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" time\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"Ir6fcXAuQ3T\",\"output_index\":1,\"sequence_number\":11}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" \",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"fqdVi89Po369Rnp\",\"output_index\":1,\"sequence_number\":12}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"12\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"yAD8Y2DOWbMZkb\",\"output_index\":1,\"sequence_number\":13}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\":\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"1rKi5urPfffrpCZ\",\"output_index\":1,\"sequence_number\":14}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"00\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"5Z5pdVFmYMIE7r\",\"output_index\":1,\"sequence_number\":15}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"obfuscation\":\"0l2cObT4L88NWom\",\"output_index\":1,\"sequence_number\":16}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":17,\"text\":\"Paris: sunny, local time 12:00.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris: sunny, local time 12:00.\"},\"sequence_number\":18}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris: sunny, local time 12:00.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":19}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_089fa9a63daf7149016a890767198c87d2a11df03750e42a8c\",\"object\":\"response\",\"created_at\":1787365223,\"status\":\"completed\",\"background\":false,\"completed_at\":1787365225,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":128,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_089fa9a63daf7149016a890767a3b887d2965e2b629f2b4fec\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqiQdpzBD55FfbDL3TzvwXzaxr3RlGf55S4sV0QCg4mSrkz4Honoc6JHbV2Tku4bnlBlpbxb3pfzMVgtDoDPQ_Bb3U-hPIhYZHIyVQLx4xDJlmVtynLpchfpFrJkQfSjMnk-lLtB3eFlsflsm1dzP9IM81yLuK7ptDUawtTBC14Gro9PM-H-C1S8xo_-3uwYGyKIYcLNU-JyKBKS2fl5-6LbA1y1-weF_zdzI4McWsRtpaty-yS-s3Qq3B0lhGd8trHY1_pIIhH9Cs3V-K1eMy39Zpe2S8VPuvYpxdrZrfi74YHcni-izHEMzYKsFMphxTTkCy8GhboweR1pSMOdNh3T0wxsFY0jxrPOSfkEnfzQzuwFZntQHGw1im9ocFqnrm-BxT0EaD0afyEBJ1DOoXQZknZtBQTfPg4_yAJwEu46SoWsFeY7wBo51K0sJePOM1JfMcfH5wMTl02iyOiFard1UMYEd0EpOiQwORZqZOghuJ-OiFYW9s2HpPDCiH0ef34UUA8o2jSih9duaGBxa4Pd9P9PI4a6S0ntRWPFF1gImYE93A2Eamt7w_WHL60ywJKctBqjGkxXTULXM8Hwoz2y5UoxHqeDlcgEPMLK-v5AX6f-eT2M6ox7HHoGnenlBzwx_GX07YME6FA5Bf-Bq0y56U__7hKqbTOVaDHFiKFN_XVUwvtw7dU0YtTdGsngMTZ93T5xT2QPPFUfzRFTFhO08sD8rd-Z3nMi9p3fgGRKEh0gGLv8_JiKcqTn_qe_3UL-s6OeTut0IK--wW0cgyUJLI48lbYDAB7W7FmyUs2ukl5hBebgIv7VNLbbT_oZc53EbLo63erV0mAqoXSo_g7zDhGGL7j129hCfj-KWoOzsmCsZMZ9RXYYyH_TrsyZdEL9uALsrQr95cPNJmJ12xcpIaJDx0ryHJXvDsBWlloJYFBM6eeRjT8SX0xN3kKQz8hk6YMySEzFs3ddonc9ZQ0E45hP3fUZXfzMyx9YOKDZn-HIXmqPHnhGq3O2C7iazoLAxtP6mABXKwsFYNwlyH82PaADhXGy6wr0ArE2iZoA-Ao9C9zlcJHBPmCa9PwX_QTv7J7pUzuZHs_YUA2htQ5vMRftSEuGkroFBUPlbsJaAOtCuEyFFXthaaezOKnBxqF8BTgcLlJJueog_7STJDuYCH5JbG8Cb2ydN02oRxkBxYT-TPIqoB-ztcZiPYOUFUalL-8bi8m6hzf5dDBzAEexWtScdCQaBfgUjNgYo-ab8AmcxS9EyyGMDXq04wazbUALwxZUeEoKw3zC1vF2EQucQukUPtfjtT97o0fUtcu1__gMiTbORdsA60d9wGFKoatAO6JBZzAUdttYlwQGJHCwMko_RuuiyiBP43GqUCq5XlYvQsrXJfAnUuxp7D-Np5Oevm7cKndhbzGP4fr_Rc7kYUpC_RtGY92oIauSV6qya_U91OamGz31Fy_eaPo6uj34qakNmIbYN4nm8jjOJvy7Yfdte_5snaeS8x6FQzNYSQTfN-qEETwAmK3iQcJDiUSkj62lgPAl49x6_MWbk2hiKfVHHkeqz9ASER-jdad9uV6yTGra84S7mZdoTrI27EFAm0UTah1PvJ2KvApnxt2bNpUaqHpoN-LzjjOsbld54v6gVOw8a6QEQ_uGyiH8gQ_H8k3JWJOVvsInpJrA2t7UuItO2Ky2hFCTwirtg16ZXU-Bx18pcqYYcaTTmWuumUucpnr7Th7XjW3C-ea4epsSeaaw==\",\"summary\":[]},{\"id\":\"msg_089fa9a63daf7149016a890769136c87d28d9f4d4d0f9450a3\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris: sunny, local time 12:00.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"none\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"description\":\"Return the benign local time for a city.\",\"name\":\"lookup_time\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":185,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":105,\"output_tokens_details\":{\"reasoning_tokens\":88},\"total_tokens\":290},\"user\":null,\"metadata\":{}},\"sequence_number\":20}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/openai-recovery/rejects-continuation-with-a-missing-parallel-tool-result.json b/packages/ai/test/fixtures/recordings/openai-recovery/rejects-continuation-with-a-missing-parallel-tool-result.json new file mode 100644 index 000000000000..58d13cf64212 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/openai-recovery/rejects-continuation-with-a-missing-parallel-tool-result.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:openai-recovery", + "provider:openai", + "protocol:openai-responses", + "tool", + "parallel", + "recovery", + "sad-path" + ], + "name": "openai-recovery/rejects-continuation-with-a-missing-parallel-tool-result", + "recordedAt": "2026-08-22T02:20:25.412Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Look up the weather and local time in Paris.\"}]},{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"I should use both lookup tools.\"}]},{\"type\":\"function_call\",\"call_id\":\"call_weather\",\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call\",\"call_id\":\"call_time\",\"name\":\"lookup_time\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_weather\",\"output\":\"sunny\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"name\":\"lookup_time\",\"description\":\"Return the benign local time for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"tool_choice\":\"none\",\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":32,\"stream\":true}" + }, + "response": { + "status": 400, + "headers": { + "content-type": "application/json" + }, + "body": "{\n \"error\": {\n \"message\": \"No tool output found for function call call_time.\",\n \"type\": \"invalid_request_error\",\n \"param\": \"input\",\n \"code\": null\n }\n}" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/xai-recovery/accepts-settled-parallel-tools-followed-by-continuation.json b/packages/ai/test/fixtures/recordings/xai-recovery/accepts-settled-parallel-tools-followed-by-continuation.json new file mode 100644 index 000000000000..c9bfed4c3852 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/xai-recovery/accepts-settled-parallel-tools-followed-by-continuation.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:xai-recovery", + "provider:xai", + "protocol:openai-responses", + "tool", + "parallel", + "recovery" + ], + "name": "xai-recovery/accepts-settled-parallel-tools-followed-by-continuation", + "recordedAt": "2026-08-22T02:20:26.712Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.x.ai/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"grok-4.6\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Look up the weather and local time in Paris.\"}]},{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"I should use both lookup tools.\"}]},{\"type\":\"function_call\",\"call_id\":\"call_weather\",\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call\",\"call_id\":\"call_time\",\"name\":\"lookup_time\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_weather\",\"output\":\"sunny\"},{\"type\":\"function_call_output\",\"call_id\":\"call_time\",\"output\":\"12:00\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"name\":\"lookup_time\",\"description\":\"Return the benign local time for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"tool_choice\":\"none\",\"store\":false,\"max_output_tokens\":128,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "event: response.created\ndata: {\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"created_at\":1787365223,\"completed_at\":null,\"id\":\"40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"max_output_tokens\":128,\"model\":\"grok-4.6\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.699999988079071,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"none\",\"tools\":[{\"type\":\"function\",\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"description\":\"Return the benign local time for a city.\",\"name\":\"lookup_time\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_p\":0.949999988079071,\"usage\":null,\"user\":null,\"incomplete_details\":null,\"status\":\"in_progress\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a7b4933f4a93564d\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\nevent: response.in_progress\ndata: {\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"created_at\":1787365223,\"completed_at\":null,\"id\":\"40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"max_output_tokens\":128,\"model\":\"grok-4.6\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.699999988079071,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"none\",\"tools\":[{\"type\":\"function\",\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"description\":\"Return the benign local time for a city.\",\"name\":\"lookup_time\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_p\":0.949999988079071,\"usage\":null,\"user\":null,\"incomplete_details\":null,\"status\":\"in_progress\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a7b4933f4a93564d\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\nevent: response.output_item.added\ndata: {\"sequence_number\":2,\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"summary\":[],\"type\":\"reasoning\",\"status\":\"in_progress\"},\"output_index\":0}\n\nevent: response.reasoning_summary_part.added\ndata: {\"sequence_number\":3,\"type\":\"response.reasoning_summary_part.added\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"part\":{\"text\":\"\",\"type\":\"summary_text\"},\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":4,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"The\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":5,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" user\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":6,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" said\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":7,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\":\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":8,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" \\\"\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":9,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"The\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":10,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" previous\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":11,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" response\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":12,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" was\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":13,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" interrupted\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":14,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\".\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":15,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" Continue\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":16,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" from\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":17,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" where\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":18,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" you\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":19,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" left\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":20,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" off\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":21,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" without\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":22,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" repeating\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":23,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" completed\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":24,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" content\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":25,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\".\\\"\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":26,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"\\n\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.done\ndata: {\"sequence_number\":27,\"type\":\"response.reasoning_summary_text.done\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"summary_index\":0,\"text\":\"The user said: \\\"The previous response was interrupted. Continue from where you left off without repeating completed content.\\\"\\n\"}\n\nevent: response.reasoning_summary_part.done\ndata: {\"sequence_number\":28,\"type\":\"response.reasoning_summary_part.done\",\"item_id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":0,\"part\":{\"text\":\"The user said: \\\"The previous response was interrupted. Continue from where you left off without repeating completed content.\\\"\\n\",\"type\":\"summary_text\"},\"summary_index\":0}\n\nevent: response.output_item.done\ndata: {\"sequence_number\":29,\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"summary\":[{\"text\":\"The user said: \\\"The previous response was interrupted. Continue from where you left off without repeating completed content.\\\"\\n\",\"type\":\"summary_text\"}],\"type\":\"reasoning\",\"status\":\"completed\"},\"output_index\":0}\n\nevent: response.output_item.added\ndata: {\"sequence_number\":30,\"type\":\"response.output_item.added\",\"item\":{\"content\":[],\"id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"in_progress\"},\"output_index\":1}\n\nevent: response.content_part.added\ndata: {\"sequence_number\":31,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"logprobs\":[],\"annotations\":[]}}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":32,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"The\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":33,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" weather\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":34,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" in\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":35,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Paris\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":36,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" is\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":37,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" sunny\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":38,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\",\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":39,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" and\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":40,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" the\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":41,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" local\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":42,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" time\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":43,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" is\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":44,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" \",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":45,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"12\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":46,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\":\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":47,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"00\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":48,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.done\ndata: {\"sequence_number\":49,\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"text\":\"The weather in Paris is sunny, and the local time is 12:00.\",\"logprobs\":[]}\n\nevent: response.content_part.done\ndata: {\"sequence_number\":50,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"text\":\"The weather in Paris is sunny, and the local time is 12:00.\",\"logprobs\":[],\"annotations\":[]}}\n\nevent: response.output_item.done\ndata: {\"sequence_number\":51,\"type\":\"response.output_item.done\",\"item\":{\"content\":[{\"type\":\"output_text\",\"text\":\"The weather in Paris is sunny, and the local time is 12:00.\",\"logprobs\":[],\"annotations\":[]}],\"id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"completed\"},\"output_index\":1}\n\nevent: response.completed\ndata: {\"sequence_number\":52,\"type\":\"response.completed\",\"response\":{\"created_at\":1787365223,\"completed_at\":1787365226,\"id\":\"40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"max_output_tokens\":128,\"model\":\"grok-4.6\",\"object\":\"response\",\"output\":[{\"id\":\"rs_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"summary\":[{\"text\":\"The user said: \\\"The previous response was interrupted. Continue from where you left off without repeating completed content.\\\"\\n\",\"type\":\"summary_text\"}],\"type\":\"reasoning\",\"status\":\"completed\"},{\"content\":[{\"type\":\"output_text\",\"text\":\"The weather in Paris is sunny, and the local time is 12:00.\",\"logprobs\":[],\"annotations\":[]}],\"id\":\"msg_40dc43df-4bdf-9105-95d5-4d0b57232a47\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"completed\"}],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.699999988079071,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"none\",\"tools\":[{\"type\":\"function\",\"description\":\"Return benign weather data for a city.\",\"name\":\"lookup_weather\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false},{\"type\":\"function\",\"description\":\"Return the benign local time for a city.\",\"name\":\"lookup_time\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"strict\":false}],\"top_p\":0.949999988079071,\"usage\":{\"input_tokens\":424,\"input_tokens_details\":{\"cached_tokens\":128},\"output_tokens\":205,\"output_tokens_details\":{\"reasoning_tokens\":188},\"total_tokens\":629,\"num_sources_used\":0,\"num_server_side_tools_used\":0,\"cost_in_usd_ticks\":18860000,\"context_details\":{\"input_tokens\":424,\"output_tokens\":205}},\"user\":null,\"incomplete_details\":null,\"status\":\"completed\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a7b4933f4a93564d\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\n" + } + } + ] +} diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index fc2456c2e1eb..cc5e9fcb51c7 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -255,7 +255,7 @@ describe("Bedrock Converse route", () => { }), ) - it.effect("merges parallel tool results into one user message", () => + it.effect("merges parallel tool results and recovery continuation into one user message", () => Effect.gen(function* () { const prepared = yield* compileRequest( LLM.request({ @@ -269,6 +269,7 @@ describe("Bedrock Converse route", () => { ]), Message.tool({ id: "tool_paris", name: "lookup", result: { forecast: "sunny" } }), Message.tool({ id: "tool_london", name: "lookup", result: { forecast: "rainy" } }), + Message.user("Continue without repeating completed calls."), ], cache: "none", }), @@ -300,6 +301,7 @@ describe("Bedrock Converse route", () => { status: "success", }, }, + { text: "Continue without repeating completed calls." }, ], }, ]) diff --git a/packages/ai/test/provider/bedrock-recovery.recorded.test.ts b/packages/ai/test/provider/bedrock-recovery.recorded.test.ts new file mode 100644 index 000000000000..7c91fafa19c3 --- /dev/null +++ b/packages/ai/test/provider/bedrock-recovery.recorded.test.ts @@ -0,0 +1,94 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { AIError, LLM, LLMEvent, Message, ToolCallPart, ToolDefinition } from "../../src/index.js" +import { configure } from "../../src/providers/amazon-bedrock.js" +import { LLMClient } from "../../src/route.js" +import { recordedTests } from "../recorded-test.js" + +const model = configure({ + apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture", + region: "us-east-2", +}).model("us.anthropic.claude-sonnet-4-6") +const continuation = + "The previous response was interrupted. Continue from where you left off without repeating completed content." +const tools = [ + ToolDefinition.make({ + name: "lookup_weather", + description: "Return benign weather data for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, + }), + ToolDefinition.make({ + name: "lookup_time", + description: "Return the benign local time for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, + }), +] +const recorded = recordedTests({ + prefix: "bedrock-recovery", + provider: "amazon-bedrock", + protocol: "bedrock-converse", + requires: ["AWS_BEARER_TOKEN_BEDROCK"], +}) + +const history = (complete: boolean) => [ + Message.user("Look up the weather and local time in Paris."), + Message.assistant([ + { type: "text" as const, text: "I should use both lookup tools." }, + ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }), + ToolCallPart.make({ id: "call_time", name: "lookup_time", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" }), + ...(complete ? [Message.tool({ id: "call_time", name: "lookup_time", result: "12:00", resultType: "text" })] : []), + Message.user(continuation), +] + +describe("Bedrock Converse interrupted recovery recorded", () => { + recorded.effect.with( + "accepts settled parallel tools followed by continuation", + { tags: ["tool", "parallel", "recovery", "bearer"] }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_bedrock_interrupted_parallel_recovery", + model, + messages: history(true), + tools, + toolChoice: "none", + generation: { maxTokens: 128, temperature: 0 }, + }), + ) + + expect(response.events.some(LLMEvent.is.finish)).toBe(true) + }), + ) + + recorded.effect.with( + "rejects continuation with a missing parallel tool result", + { tags: ["tool", "parallel", "recovery", "bearer", "sad-path"] }, + () => + Effect.gen(function* () { + const error = yield* LLMClient.generate( + LLM.request({ + id: "recorded_bedrock_incomplete_parallel_recovery", + model, + messages: history(false), + tools, + toolChoice: "none", + generation: { maxTokens: 32, temperature: 0 }, + }), + ).pipe(Effect.flip) + + expect(error).toBeInstanceOf(AIError) + expect(error.reason).toMatchObject({ _tag: "InvalidRequest" }) + }), + ) +}) diff --git a/packages/ai/test/provider/meta-recovery.recorded.test.ts b/packages/ai/test/provider/meta-recovery.recorded.test.ts new file mode 100644 index 000000000000..506675feec4f --- /dev/null +++ b/packages/ai/test/provider/meta-recovery.recorded.test.ts @@ -0,0 +1,76 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { AIError, LLM, LLMEvent, Message, ToolCallPart, ToolDefinition } from "../../src/index.js" +import { configure } from "../../src/providers/openai.js" +import { LLMClient } from "../../src/route.js" +import { recordedTests } from "../recorded-test.js" + +const model = configure({ + apiKey: process.env.META_API_KEY ?? "fixture", + baseURL: "https://api.meta.ai/v1", +}).responses("muse-spark-1.2") +const continuation = + "The previous response was interrupted. Continue from where you left off without repeating completed content." +const lookup = ToolDefinition.make({ + name: "lookup_weather", + description: "Return benign weather data for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, +}) +const recorded = recordedTests({ + prefix: "meta-recovery", + provider: "meta", + protocol: "openai-responses", + requires: ["META_API_KEY"], +}) + +const history = (settled: boolean) => [ + Message.user("Look up the weather in Paris."), + Message.assistant([ + { type: "text" as const, text: "I should use the weather tool." }, + ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }), + ]), + ...(settled + ? [Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" })] + : []), + Message.user(continuation), +] + +describe("Meta Responses interrupted recovery recorded", () => { + recorded.effect.with("accepts a settled tool followed by continuation", { tags: ["tool", "recovery"] }, () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_meta_interrupted_tool_recovery", + model, + messages: history(true), + tools: [lookup], + generation: { maxTokens: 128 }, + }), + ) + + expect(response.events.some(LLMEvent.is.finish)).toBe(true) + }), + ) + + recorded.effect.with("rejects continuation with an unresolved tool", { tags: ["tool", "recovery", "sad-path"] }, () => + Effect.gen(function* () { + const error = yield* LLMClient.generate( + LLM.request({ + id: "recorded_meta_unresolved_tool_recovery", + model, + messages: history(false), + tools: [lookup], + generation: { maxTokens: 32 }, + }), + ).pipe(Effect.flip) + + expect(error).toBeInstanceOf(AIError) + expect(error.reason).toMatchObject({ _tag: "InvalidRequest" }) + expect(error.reason.message).toContain("No tool output found") + }), + ) +}) diff --git a/packages/ai/test/provider/openai-recovery.recorded.test.ts b/packages/ai/test/provider/openai-recovery.recorded.test.ts new file mode 100644 index 000000000000..5577d26a2f6d --- /dev/null +++ b/packages/ai/test/provider/openai-recovery.recorded.test.ts @@ -0,0 +1,92 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { AIError, LLM, LLMEvent, Message, ToolCallPart, ToolDefinition } from "../../src/index.js" +import { configure } from "../../src/providers/openai.js" +import { LLMClient } from "../../src/route.js" +import { recordedTests } from "../recorded-test.js" + +const model = configure({ apiKey: process.env.OPENAI_API_KEY ?? "fixture" }).responses("gpt-5.5") +const continuation = + "The previous response was interrupted. Continue from where you left off without repeating completed content." +const tools = [ + ToolDefinition.make({ + name: "lookup_weather", + description: "Return benign weather data for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, + }), + ToolDefinition.make({ + name: "lookup_time", + description: "Return the benign local time for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, + }), +] +const recorded = recordedTests({ + prefix: "openai-recovery", + provider: "openai", + protocol: "openai-responses", + requires: ["OPENAI_API_KEY"], +}) + +const history = (complete: boolean) => [ + Message.user("Look up the weather and local time in Paris."), + Message.assistant([ + { type: "text" as const, text: "I should use both lookup tools." }, + ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }), + ToolCallPart.make({ id: "call_time", name: "lookup_time", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" }), + ...(complete ? [Message.tool({ id: "call_time", name: "lookup_time", result: "12:00", resultType: "text" })] : []), + Message.user(continuation), +] + +describe("OpenAI Responses interrupted recovery recorded", () => { + recorded.effect.with( + "accepts settled parallel tools followed by continuation", + { tags: ["tool", "parallel", "recovery"] }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_openai_interrupted_parallel_recovery", + model, + messages: history(true), + tools, + toolChoice: "none", + generation: { maxTokens: 128 }, + }), + ) + + expect(response.events.some(LLMEvent.is.finish)).toBe(true) + }), + ) + + recorded.effect.with( + "rejects continuation with a missing parallel tool result", + { tags: ["tool", "parallel", "recovery", "sad-path"] }, + () => + Effect.gen(function* () { + const error = yield* LLMClient.generate( + LLM.request({ + id: "recorded_openai_incomplete_parallel_recovery", + model, + messages: history(false), + tools, + toolChoice: "none", + generation: { maxTokens: 32 }, + }), + ).pipe(Effect.flip) + + expect(error).toBeInstanceOf(AIError) + expect(error.reason).toMatchObject({ _tag: "InvalidRequest" }) + expect(error.reason.message).toContain("No tool output found") + }), + ) +}) diff --git a/packages/ai/test/provider/xai-recovery.recorded.test.ts b/packages/ai/test/provider/xai-recovery.recorded.test.ts new file mode 100644 index 000000000000..91553bed6f62 --- /dev/null +++ b/packages/ai/test/provider/xai-recovery.recorded.test.ts @@ -0,0 +1,68 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM, LLMEvent, Message, ToolCallPart, ToolDefinition } from "../../src/index.js" +import { configure } from "../../src/providers/xai.js" +import { LLMClient } from "../../src/route.js" +import { recordedTests } from "../recorded-test.js" + +const model = configure({ apiKey: process.env.XAI_API_KEY ?? "fixture" }).responses("grok-4.6") +const continuation = + "The previous response was interrupted. Continue from where you left off without repeating completed content." +const tools = [ + ToolDefinition.make({ + name: "lookup_weather", + description: "Return benign weather data for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, + }), + ToolDefinition.make({ + name: "lookup_time", + description: "Return the benign local time for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, + }), +] +const recorded = recordedTests({ + prefix: "xai-recovery", + provider: "xai", + protocol: "openai-responses", + requires: ["XAI_API_KEY"], +}) + +describe("xAI Responses interrupted recovery recorded", () => { + recorded.effect.with( + "accepts settled parallel tools followed by continuation", + { tags: ["tool", "parallel", "recovery"] }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_xai_interrupted_parallel_recovery", + model, + messages: [ + Message.user("Look up the weather and local time in Paris."), + Message.assistant([ + { type: "text", text: "I should use both lookup tools." }, + ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }), + ToolCallPart.make({ id: "call_time", name: "lookup_time", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" }), + Message.tool({ id: "call_time", name: "lookup_time", result: "12:00", resultType: "text" }), + Message.user(continuation), + ], + tools, + toolChoice: "none", + generation: { maxTokens: 128 }, + }), + ) + + expect(response.events.some(LLMEvent.is.finish)).toBe(true) + }), + ) +})