refactor(llm): redesign error model as flat tagged union
- Replace LLMError { module, method, reason } wrapper with a flat tagged
union: BadRequest, Authentication, PermissionDenied, NotFound, RateLimit,
QuotaExceeded, ContentPolicy, ContextOverflow, ServerError, APIError,
ConnectionError, TimeoutError, MalformedResponse, NoRoute.
- Delete the provider-error LLMEvent: streams carry output only and every
failure exits through the typed error channel.
- Add one shared classifyApiFailure classifier used by the HTTP executor,
protocol stream errors, and the AI SDK adapter so all routes classify
identically (including OpenAI in-stream rate_limit_exceeded and
internal_error codes).
- Enforce a terminal contract in LLMClient.stream for every route: EOF
without finish and output after finish fail as MalformedResponse.
- Classify AI SDK failures properly in core/aisdk.ts instead of collapsing
to UnknownProvider; preserve status, headers, body, and retry-after.
- Simplify the session runner: drop held-back overflow events, key overflow
recovery off LLM.ContextOverflow, retry RateLimit | ServerError |
ConnectionError | TimeoutError.
- Map new tags in toSessionError (provider.context-overflow,
provider.timeout, provider.not-found).
This commit is contained in:
parent
768a69bbbd
commit
bd51cdba12
37 changed files with 776 additions and 676 deletions
|
|
@ -1,18 +1,22 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
AuthenticationReason,
|
||||
ContentPolicyReason,
|
||||
InvalidProviderOutputReason,
|
||||
InvalidRequestReason,
|
||||
LLMError,
|
||||
NoRouteReason,
|
||||
APIError,
|
||||
Authentication,
|
||||
BadRequest,
|
||||
ConnectionError,
|
||||
ContentPolicy,
|
||||
ContextOverflow,
|
||||
MalformedResponse,
|
||||
ModelID,
|
||||
NoRoute,
|
||||
NotFound,
|
||||
PermissionDenied,
|
||||
ProviderID,
|
||||
ProviderInternalReason,
|
||||
QuotaExceededReason,
|
||||
RateLimitReason,
|
||||
TransportReason,
|
||||
UnknownProviderReason,
|
||||
QuotaExceeded,
|
||||
RateLimit,
|
||||
RouteID,
|
||||
ServerError,
|
||||
TimeoutError,
|
||||
ToolFailure,
|
||||
} from "@opencode-ai/llm"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
|
|
@ -20,39 +24,33 @@ import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
|
|||
import { toSessionError } from "@opencode-ai/core/session/to-session-error"
|
||||
import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
|
||||
|
||||
const llm = (reason: LLMError["reason"]) => new LLMError({ module: "test", method: "stream", reason })
|
||||
|
||||
describe("toSessionError", () => {
|
||||
test("maps every LLM reason to the open wire type", () => {
|
||||
expect(toSessionError(llm(new RateLimitReason({ message: "rate", retryAfterMs: 123 })))).toEqual({
|
||||
test("maps every LLM error tag to the open wire type", () => {
|
||||
expect(toSessionError(new RateLimit({ message: "rate", retryAfterMs: 123 }))).toEqual({
|
||||
type: "provider.rate-limit",
|
||||
message: "rate",
|
||||
})
|
||||
expect(toSessionError(llm(new AuthenticationReason({ message: "auth", kind: "invalid" }))).type).toBe(
|
||||
"provider.auth",
|
||||
)
|
||||
expect(toSessionError(llm(new QuotaExceededReason({ message: "quota" }))).type).toBe("provider.quota")
|
||||
expect(toSessionError(llm(new ContentPolicyReason({ message: "blocked" }))).type).toBe("provider.content-filter")
|
||||
expect(toSessionError(llm(new TransportReason({ message: "transport" }))).type).toBe("provider.transport")
|
||||
expect(toSessionError(llm(new ProviderInternalReason({ message: "internal", status: 500 }))).type).toBe(
|
||||
"provider.internal",
|
||||
)
|
||||
expect(toSessionError(llm(new InvalidProviderOutputReason({ message: "output" }))).type).toBe(
|
||||
"provider.invalid-output",
|
||||
)
|
||||
expect(toSessionError(llm(new InvalidRequestReason({ message: "request" }))).type).toBe("provider.invalid-request")
|
||||
expect(toSessionError(new Authentication({ message: "auth" })).type).toBe("provider.auth")
|
||||
expect(toSessionError(new PermissionDenied({ message: "forbidden" })).type).toBe("provider.auth")
|
||||
expect(toSessionError(new NotFound({ message: "missing" })).type).toBe("provider.not-found")
|
||||
expect(toSessionError(new QuotaExceeded({ message: "quota" })).type).toBe("provider.quota")
|
||||
expect(toSessionError(new ContentPolicy({ message: "blocked" })).type).toBe("provider.content-filter")
|
||||
expect(toSessionError(new ContextOverflow({ message: "too long" })).type).toBe("provider.context-overflow")
|
||||
expect(toSessionError(new ConnectionError({ message: "reset" })).type).toBe("provider.transport")
|
||||
expect(toSessionError(new TimeoutError({ message: "timed out" })).type).toBe("provider.timeout")
|
||||
expect(toSessionError(new ServerError({ message: "internal", status: 500 })).type).toBe("provider.internal")
|
||||
expect(toSessionError(new MalformedResponse({ message: "output" })).type).toBe("provider.invalid-output")
|
||||
expect(toSessionError(new BadRequest({ message: "request" })).type).toBe("provider.invalid-request")
|
||||
expect(
|
||||
toSessionError(
|
||||
llm(
|
||||
new NoRouteReason({
|
||||
route: "route",
|
||||
provider: ProviderID.make("provider"),
|
||||
model: ModelID.make("model"),
|
||||
}),
|
||||
),
|
||||
new NoRoute({
|
||||
route: RouteID.make("route"),
|
||||
provider: ProviderID.make("provider"),
|
||||
model: ModelID.make("model"),
|
||||
}),
|
||||
).type,
|
||||
).toBe("provider.no-route")
|
||||
expect(toSessionError(llm(new UnknownProviderReason({ message: "unknown" }))).type).toBe("provider.unknown")
|
||||
expect(toSessionError(new APIError({ message: "unknown", status: 418 })).type).toBe("provider.unknown")
|
||||
})
|
||||
|
||||
test("preserves the permission rejection type without exposing internal fields", () => {
|
||||
|
|
@ -71,23 +69,31 @@ describe("toSessionError", () => {
|
|||
})
|
||||
})
|
||||
|
||||
test("retries only rate limits, provider-internal failures, and transport failures", () => {
|
||||
test("retries only rate limits, server errors, connection failures, and timeouts", () => {
|
||||
const eligible = [
|
||||
llm(new RateLimitReason({ message: "rate" })),
|
||||
llm(new ProviderInternalReason({ message: "internal", status: 500 })),
|
||||
llm(new TransportReason({ message: "transport" })),
|
||||
new RateLimit({ message: "rate" }),
|
||||
new ServerError({ message: "internal", status: 500 }),
|
||||
new ConnectionError({ message: "reset" }),
|
||||
new TimeoutError({ message: "timed out" }),
|
||||
]
|
||||
const ineligible = [
|
||||
llm(new AuthenticationReason({ message: "auth", kind: "invalid" })),
|
||||
llm(new QuotaExceededReason({ message: "quota" })),
|
||||
llm(new ContentPolicyReason({ message: "blocked" })),
|
||||
llm(new InvalidProviderOutputReason({ message: "output" })),
|
||||
llm(new InvalidRequestReason({ message: "request" })),
|
||||
llm(new NoRouteReason({ route: "route", provider: ProviderID.make("provider"), model: ModelID.make("model") })),
|
||||
llm(new UnknownProviderReason({ message: "unknown" })),
|
||||
new Authentication({ message: "auth" }),
|
||||
new PermissionDenied({ message: "forbidden" }),
|
||||
new NotFound({ message: "missing" }),
|
||||
new QuotaExceeded({ message: "quota" }),
|
||||
new ContentPolicy({ message: "blocked" }),
|
||||
new ContextOverflow({ message: "too long" }),
|
||||
new MalformedResponse({ message: "output" }),
|
||||
new BadRequest({ message: "request" }),
|
||||
new NoRoute({
|
||||
route: RouteID.make("route"),
|
||||
provider: ProviderID.make("provider"),
|
||||
model: ModelID.make("model"),
|
||||
}),
|
||||
new APIError({ message: "unknown" }),
|
||||
]
|
||||
|
||||
expect(eligible.map(SessionRunnerRetry.isRetryable)).toEqual([true, true, true])
|
||||
expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual([false, false, false, false, false, false, false])
|
||||
expect(eligible.map(SessionRunnerRetry.isRetryable)).toEqual([true, true, true, true])
|
||||
expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual(ineligible.map(() => false))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { LLMError, TransportReason } from "@opencode-ai/llm"
|
||||
import { ConnectionError } from "@opencode-ai/llm"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
|
@ -25,17 +25,10 @@ const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Event
|
|||
describe("SessionExecution lifecycle", () => {
|
||||
test("classifies success and typed failure terminals", () => {
|
||||
expect(SessionExecution.terminal(Exit.succeed(undefined))).toEqual({ type: "succeeded" })
|
||||
expect(
|
||||
SessionExecution.terminal(
|
||||
Exit.fail(
|
||||
new LLMError({
|
||||
module: "test",
|
||||
method: "stream",
|
||||
reason: new TransportReason({ message: "Disconnected" }),
|
||||
}),
|
||||
),
|
||||
),
|
||||
).toEqual({ type: "failed", error: { type: "provider.transport", message: "Disconnected" } })
|
||||
expect(SessionExecution.terminal(Exit.fail(new ConnectionError({ message: "Disconnected" })))).toEqual({
|
||||
type: "failed",
|
||||
error: { type: "provider.transport", message: "Disconnected" },
|
||||
})
|
||||
const storage = new ToolOutputStore.StorageError({ operation: "encode", cause: new Error("invalid output") })
|
||||
expect(SessionExecution.terminal(Exit.fail(storage))).toEqual({
|
||||
type: "failed",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
APIError,
|
||||
BadRequest,
|
||||
ConnectionError,
|
||||
ContextOverflow,
|
||||
LLMClient,
|
||||
LLMError,
|
||||
LLMEvent,
|
||||
Model,
|
||||
RateLimit,
|
||||
ToolFailure,
|
||||
TransportReason,
|
||||
InvalidRequestReason,
|
||||
RateLimitReason,
|
||||
type LLMClientShape,
|
||||
type LLMError,
|
||||
type LLMRequest,
|
||||
} from "@opencode-ai/llm"
|
||||
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
|
||||
|
|
@ -68,8 +70,9 @@ import { asc, eq } from "drizzle-orm"
|
|||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const requests: LLMRequest[] = []
|
||||
type ScriptedResponse = LLMEvent[] | Stream.Stream<LLMEvent, LLMError>
|
||||
let response: LLMEvent[] = []
|
||||
let responses: LLMEvent[][] | undefined
|
||||
let responses: ScriptedResponse[] | undefined
|
||||
let responseStream: Stream.Stream<LLMEvent, LLMError> | undefined
|
||||
let responseStreams: Stream.Stream<LLMEvent, LLMError>[] | undefined
|
||||
let streamGate: Deferred.Deferred<void> | undefined
|
||||
|
|
@ -92,9 +95,12 @@ const client = Layer.succeed(
|
|||
responseStream = undefined
|
||||
return stream
|
||||
}
|
||||
const scripted = responses === undefined ? response : (responses.shift() ?? [])
|
||||
const events = streamFailure
|
||||
? Stream.fail(streamFailure)
|
||||
: Stream.fromIterable(responses === undefined ? response : (responses.shift() ?? []))
|
||||
: Array.isArray(scripted)
|
||||
? Stream.fromIterable(scripted)
|
||||
: scripted
|
||||
if (!streamGate) return events
|
||||
return Stream.unwrap(
|
||||
(streamStarted ? Deferred.succeed(streamStarted, undefined) : Effect.void).pipe(
|
||||
|
|
@ -480,26 +486,16 @@ const setup = Effect.gen(function* () {
|
|||
return yield* SessionV2.Service
|
||||
})
|
||||
|
||||
const providerUnavailable = () =>
|
||||
new LLMError({
|
||||
module: "test",
|
||||
method: "stream",
|
||||
reason: new TransportReason({ message: "Provider unavailable" }),
|
||||
})
|
||||
const providerUnavailable = () => new ConnectionError({ message: "Provider unavailable" })
|
||||
|
||||
const invalidRequest = () =>
|
||||
new LLMError({
|
||||
module: "test",
|
||||
method: "stream",
|
||||
reason: new InvalidRequestReason({ message: "Invalid request" }),
|
||||
})
|
||||
const contextOverflow = () => new ContextOverflow({ message: "prompt too long" })
|
||||
|
||||
const rateLimited = (retryAfterMs?: number) =>
|
||||
new LLMError({
|
||||
module: "test",
|
||||
method: "stream",
|
||||
reason: new RateLimitReason({ message: "Rate limited", retryAfterMs }),
|
||||
})
|
||||
const failingResponse = (events: LLMEvent[], failure: LLMError): Stream.Stream<LLMEvent, LLMError> =>
|
||||
Stream.fromIterable(events).pipe(Stream.concat(Stream.fail(failure)))
|
||||
|
||||
const invalidRequest = () => new BadRequest({ message: "Invalid request" })
|
||||
|
||||
const rateLimited = (retryAfterMs?: number) => new RateLimit({ message: "Rate limited", retryAfterMs })
|
||||
|
||||
const setupOverflowRecovery = Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
|
|
@ -1646,14 +1642,14 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* admit(session, "Earlier question")
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
response = [LLMEvent.providerError({ message: "summary unavailable" })]
|
||||
responseStream = Stream.fail(new APIError({ message: "summary unavailable" }))
|
||||
const compaction = yield* session.compact({ sessionID })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
error: { type: "provider.error", message: "summary unavailable" },
|
||||
error: { type: "provider.unknown", message: "summary unavailable" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
|
@ -1763,7 +1759,7 @@ describe("SessionRunnerLLM", () => {
|
|||
currentModel = compactModel
|
||||
requests.length = 0
|
||||
responses = [
|
||||
[LLMEvent.providerError({ message: "Unsupported parameter: max_output_tokens" })],
|
||||
Stream.fail(new BadRequest({ message: "Unsupported parameter: max_output_tokens" })),
|
||||
reply.text("Must not run", "text-after-failed-compaction"),
|
||||
]
|
||||
yield* admit(session, "Recent exact request ".repeat(180))
|
||||
|
|
@ -1786,10 +1782,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
const session = yield* setupOverflowRecovery
|
||||
responses = [
|
||||
[
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
|
||||
],
|
||||
failingResponse([LLMEvent.stepStart({ index: 0 })], contextOverflow()),
|
||||
reply.text("## Objective\n- Recover overflow", "text-summary"),
|
||||
reply.text("Recovered", "text-final"),
|
||||
]
|
||||
|
|
@ -1816,7 +1809,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setupOverflowRecovery
|
||||
currentModel = model
|
||||
responses = [
|
||||
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||
Stream.fail(contextOverflow()),
|
||||
reply.text("## Objective\n- Recover unknown limit", "text-summary-unknown-limit"),
|
||||
reply.text("Recovered", "text-final-unknown-limit"),
|
||||
]
|
||||
|
|
@ -1836,7 +1829,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setupOverflowRecovery
|
||||
currentModel = undersizedContextModel
|
||||
responses = [
|
||||
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||
Stream.fail(contextOverflow()),
|
||||
reply.text("## Objective\n- Recover undersized limit", "text-summary-undersized-limit"),
|
||||
reply.text("Recovered", "text-final-undersized-limit"),
|
||||
]
|
||||
|
|
@ -1854,10 +1847,7 @@ describe("SessionRunnerLLM", () => {
|
|||
it.effect("persists a second context overflow after one recovery", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setupOverflowRecovery
|
||||
const overflow = () => [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
|
||||
]
|
||||
const overflow = () => failingResponse([LLMEvent.stepStart({ index: 0 })], contextOverflow())
|
||||
responses = [overflow(), reply.text("## Objective\n- Recover once", "text-summary"), overflow()]
|
||||
yield* admit(session, "Continue")
|
||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
|
||||
|
|
@ -1873,16 +1863,7 @@ describe("SessionRunnerLLM", () => {
|
|||
it.effect("recovers once from a raw context overflow failure", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setupOverflowRecovery
|
||||
responseStream = Stream.fail(
|
||||
new LLMError({
|
||||
module: "test",
|
||||
method: "stream",
|
||||
reason: new InvalidRequestReason({
|
||||
message: "prompt too long",
|
||||
classification: "context-overflow",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
responseStream = Stream.fail(contextOverflow())
|
||||
responses = [
|
||||
reply.text("## Objective\n- Recover raw overflow", "text-summary"),
|
||||
reply.text("Recovered", "text-final"),
|
||||
|
|
@ -1901,10 +1882,7 @@ describe("SessionRunnerLLM", () => {
|
|||
it.effect("publishes the original overflow when recovery summarization fails", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setupOverflowRecovery
|
||||
responses = [
|
||||
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||
[LLMEvent.providerError({ message: "summary unavailable" })],
|
||||
]
|
||||
responses = [Stream.fail(contextOverflow()), Stream.fail(new APIError({ message: "summary unavailable" }))]
|
||||
yield* admit(session, "Continue")
|
||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
|
||||
|
||||
|
|
@ -1915,7 +1893,7 @@ describe("SessionRunnerLLM", () => {
|
|||
type: "compaction",
|
||||
status: "failed",
|
||||
reason: "auto",
|
||||
error: { type: "provider.error", message: "summary unavailable" },
|
||||
error: { type: "provider.unknown", message: "summary unavailable" },
|
||||
}),
|
||||
)
|
||||
expect(context.slice(-3)).toMatchObject([
|
||||
|
|
@ -1929,10 +1907,7 @@ describe("SessionRunnerLLM", () => {
|
|||
it.effect("interrupts overflow recovery while the summary provider is running", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setupOverflowRecovery
|
||||
responses = [
|
||||
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||
reply.text("## Objective\n- Interrupted", "text-summary"),
|
||||
]
|
||||
responses = [Stream.fail(contextOverflow()), reply.text("## Objective\n- Interrupted", "text-summary")]
|
||||
const firstGate = yield* Deferred.make<void>()
|
||||
const summaryGate = yield* Deferred.make<void>()
|
||||
streamGate = firstGate
|
||||
|
|
@ -3515,7 +3490,10 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setup
|
||||
yield* admit(session, "Fail durably")
|
||||
|
||||
response = [LLMEvent.stepStart({ index: 0 }), LLMEvent.providerError({ message: "Provider unavailable" })]
|
||||
responseStream = failingResponse(
|
||||
[LLMEvent.stepStart({ index: 0 })],
|
||||
new APIError({ message: "Provider unavailable" }),
|
||||
)
|
||||
|
||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
|
||||
|
||||
|
|
@ -3532,7 +3510,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setup
|
||||
yield* admit(session, "Fail before step")
|
||||
|
||||
response = [LLMEvent.providerError({ message: "Provider unavailable" })]
|
||||
responseStream = Stream.fail(new APIError({ message: "Provider unavailable" }))
|
||||
|
||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
|
||||
|
||||
|
|
@ -3620,13 +3598,15 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setup
|
||||
yield* admit(session, "Fail after output")
|
||||
|
||||
response = [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.textStart({ id: "text-partial" }),
|
||||
LLMEvent.textDelta({ id: "text-partial", text: "Partial" }),
|
||||
LLMEvent.textEnd({ id: "text-partial" }),
|
||||
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
|
||||
]
|
||||
responseStream = failingResponse(
|
||||
[
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.textStart({ id: "text-partial" }),
|
||||
LLMEvent.textDelta({ id: "text-partial", text: "Partial" }),
|
||||
LLMEvent.textEnd({ id: "text-partial" }),
|
||||
],
|
||||
contextOverflow(),
|
||||
)
|
||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
|
|
@ -3784,11 +3764,13 @@ describe("SessionRunnerLLM", () => {
|
|||
toolExecutionGate = yield* Deferred.make<void>()
|
||||
toolExecutionsStarted = yield* Deferred.make<void>()
|
||||
toolExecutionsReady = 1
|
||||
response = [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolCall({ id: "call-before-provider-error", name: "echo", input: { text: "settled" } }),
|
||||
LLMEvent.providerError({ message: "Provider unavailable" }),
|
||||
]
|
||||
responseStream = failingResponse(
|
||||
[
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolCall({ id: "call-before-provider-error", name: "echo", input: { text: "settled" } }),
|
||||
],
|
||||
new APIError({ message: "Provider unavailable" }),
|
||||
)
|
||||
|
||||
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(toolExecutionsStarted)
|
||||
|
|
@ -3815,11 +3797,10 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setup
|
||||
yield* admit(session, "Fail hosted tool durably")
|
||||
|
||||
response = [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
hostedCall("call-hosted-provider-error", "effect"),
|
||||
LLMEvent.providerError({ message: "Provider unavailable" }),
|
||||
]
|
||||
responseStream = failingResponse(
|
||||
[LLMEvent.stepStart({ index: 0 }), hostedCall("call-hosted-provider-error", "effect")],
|
||||
new APIError({ message: "Provider unavailable" }),
|
||||
)
|
||||
|
||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
|
||||
|
||||
|
|
@ -3846,11 +3827,13 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
yield* admit(session, "Defect while provider fails")
|
||||
response = [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolCall({ id: "call-defect-provider-error", name: "defect", input: {} }),
|
||||
LLMEvent.providerError({ message: "Provider unavailable" }),
|
||||
]
|
||||
responseStream = failingResponse(
|
||||
[
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolCall({ id: "call-defect-provider-error", name: "defect", input: {} }),
|
||||
],
|
||||
new APIError({ message: "Provider unavailable" }),
|
||||
)
|
||||
|
||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue