Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7357352e4 | |||
| 56faca8e58 |
@@ -305,14 +305,7 @@ export const layer = Layer.effect(
|
||||
const llmFailure = failure instanceof LLMError ? failure : undefined
|
||||
if (llmFailure && !publisher.hasProviderError()) {
|
||||
yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true))
|
||||
yield* withPublication(
|
||||
events.publish(SessionEvent.Step.Failed, {
|
||||
sessionID: session.id,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: yield* publisher.startAssistant(),
|
||||
error: { type: "unknown", message: llmFailure.reason.message },
|
||||
}),
|
||||
)
|
||||
yield* withPublication(publisher.failAssistant(llmFailure.reason.message))
|
||||
}
|
||||
if (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) yield* FiberSet.clear(toolFibers)
|
||||
const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit)
|
||||
@@ -327,6 +320,8 @@ export const layer = Layer.effect(
|
||||
) {
|
||||
yield* FiberSet.clear(toolFibers)
|
||||
yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted"))
|
||||
if (publisher.hasAssistantStarted())
|
||||
yield* withPublication(publisher.failAssistant("Provider turn interrupted"))
|
||||
}
|
||||
if (settled._tag === "Failure" && !Cause.hasInterrupts(settled.cause)) {
|
||||
const failure = Cause.squash(settled.cause)
|
||||
|
||||
@@ -66,6 +66,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
||||
const timestamp = DateTime.now
|
||||
let assistantMessageID: SessionMessage.ID | undefined
|
||||
let providerFailed = false
|
||||
let assistantSettled = false
|
||||
|
||||
const startAssistant = Effect.fnUntraced(function* () {
|
||||
if (assistantMessageID !== undefined) return assistantMessageID
|
||||
@@ -156,6 +157,18 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
||||
yield* toolInput.flush()
|
||||
})
|
||||
|
||||
const failAssistant = Effect.fnUntraced(function* (error: string) {
|
||||
if (assistantSettled) return
|
||||
yield* flushFragments()
|
||||
assistantSettled = true
|
||||
yield* events.publish(SessionEvent.Step.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
error: { type: "unknown", message: error },
|
||||
})
|
||||
})
|
||||
|
||||
const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
|
||||
if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`)
|
||||
const assistantMessageID = yield* startAssistant()
|
||||
@@ -375,6 +388,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
||||
}
|
||||
case "step-finish":
|
||||
yield* flush()
|
||||
assistantSettled = true
|
||||
yield* events.publish(SessionEvent.Step.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
@@ -388,13 +402,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
||||
return
|
||||
case "provider-error":
|
||||
providerFailed = true
|
||||
yield* flush()
|
||||
yield* events.publish(SessionEvent.Step.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
error: { type: "unknown", message: event.message },
|
||||
})
|
||||
yield* failAssistant(event.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
@@ -402,6 +410,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
||||
return {
|
||||
publish,
|
||||
flush,
|
||||
failAssistant,
|
||||
failUnsettledTools,
|
||||
hasAssistantStarted: () => assistantMessageID !== undefined,
|
||||
hasProviderError: () => providerFailed,
|
||||
|
||||
@@ -75,7 +75,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
||||
if (item.type === "reasoning")
|
||||
return sameModel
|
||||
? [{ type: "reasoning", text: item.text, providerMetadata: item.providerMetadata }]
|
||||
: item.text.length > 0
|
||||
: item.text.trim().length > 0
|
||||
? [{ type: "text", text: item.text }]
|
||||
: []
|
||||
const call = toolCall(item, sameModel ? item.provider?.metadata : undefined)
|
||||
|
||||
@@ -14,6 +14,36 @@ const id = (value: string) => SessionMessage.ID.make(`msg_${value}`)
|
||||
const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route })
|
||||
|
||||
describe("toLLMMessages", () => {
|
||||
test("preserves projected assistant turns for request compilation", () => {
|
||||
const assistant = (value: string, content: SessionMessage.Assistant["content"]) =>
|
||||
new SessionMessage.Assistant({
|
||||
id: id(value),
|
||||
type: "assistant",
|
||||
agent: "build",
|
||||
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
|
||||
content,
|
||||
time: { created, completed: created },
|
||||
})
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
assistant("empty", []),
|
||||
assistant("empty-text", [new SessionMessage.AssistantText({ type: "text", id: "text-empty", text: "" })]),
|
||||
assistant("text", [new SessionMessage.AssistantText({ type: "text", id: "text", text: "Partial" })]),
|
||||
assistant("reasoning", [
|
||||
new SessionMessage.AssistantReasoning({
|
||||
type: "reasoning",
|
||||
id: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: { anthropic: { signature: "sig_1" } },
|
||||
}),
|
||||
]),
|
||||
],
|
||||
model,
|
||||
)
|
||||
|
||||
expect(messages.map((message) => message.id)).toEqual([id("empty"), id("empty-text"), id("text"), id("reasoning")])
|
||||
})
|
||||
|
||||
test("maps every top-level V2 Session message type", () => {
|
||||
const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
|
||||
const messages = toLLMMessages(
|
||||
|
||||
@@ -547,6 +547,8 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
|
||||
{ type: "user", text: prompt },
|
||||
{
|
||||
type: "assistant",
|
||||
finish: "error",
|
||||
error: { type: "unknown", message: "Provider turn interrupted" },
|
||||
content: [
|
||||
kind === "tool input"
|
||||
? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } }
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
HttpOptions,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
Message,
|
||||
Model,
|
||||
ModelLimits,
|
||||
LLMError as LLMErrorClass,
|
||||
@@ -172,6 +173,41 @@ const resolveRequestOptions = (request: LLMRequest) =>
|
||||
http: mergeHttpOptions(request.model.route.defaults.http, request.http),
|
||||
})
|
||||
|
||||
const meaningfulAssistantPart = (part: Message["content"][number]) => {
|
||||
if (part.type === "text") return part.text !== ""
|
||||
if (part.type !== "reasoning") return true
|
||||
return (
|
||||
part.text.trim().length > 0 ||
|
||||
part.encrypted !== undefined ||
|
||||
(part.providerMetadata !== undefined && Object.keys(part.providerMetadata).length > 0)
|
||||
)
|
||||
}
|
||||
|
||||
const omitEmptyAssistantMessages = (request: LLMRequest) => {
|
||||
let changed = false
|
||||
const messages = request.messages.flatMap((message) => {
|
||||
if (message.role !== "assistant") return [message]
|
||||
const content = message.content.filter(meaningfulAssistantPart)
|
||||
if (content.length === 0 && (!message.native || Object.keys(message.native).length === 0)) {
|
||||
changed = true
|
||||
return []
|
||||
}
|
||||
if (content.length === message.content.length) return [message]
|
||||
changed = true
|
||||
return [
|
||||
Message.make({
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
content,
|
||||
metadata: message.metadata,
|
||||
native: message.native,
|
||||
}),
|
||||
]
|
||||
})
|
||||
if (!changed) return request
|
||||
return LLMRequest.update(request, { messages })
|
||||
}
|
||||
|
||||
export interface MakeInput<Body, Frame, Event, State> {
|
||||
/** Route id used in diagnostics and prepared request metadata. */
|
||||
readonly id: string
|
||||
@@ -335,7 +371,7 @@ export function make<Body, Prepared, Frame, Event, State>(
|
||||
// validated provider body plus transport-private prepared data, but does not
|
||||
// execute transport.
|
||||
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
|
||||
const resolved = applyCachePolicy(resolveRequestOptions(request))
|
||||
const resolved = applyCachePolicy(omitEmptyAssistantMessages(resolveRequestOptions(request)))
|
||||
const route = resolved.model.route
|
||||
|
||||
const body = yield* route.body
|
||||
|
||||
@@ -57,6 +57,30 @@ describe("Anthropic Messages route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("removes empty assistant parts while preserving tool calls", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{ type: "text", text: "" },
|
||||
ToolCallPart.make({ id: "call_1", name: "read", input: { path: "README.md" } }),
|
||||
]),
|
||||
],
|
||||
cache: "none",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call_1", name: "read", input: { path: "README.md" } }],
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
|
||||
@@ -92,6 +92,48 @@ describe("OpenAI Chat route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("omits empty assistant turns before protocol serialization", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.user("Before"), Message.assistant(""), Message.user("After")],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{ role: "user", content: "Before" },
|
||||
{ role: "user", content: "After" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves opaque OpenAI-compatible reasoning continuation", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
Message.make({
|
||||
role: "assistant",
|
||||
content: [],
|
||||
native: { openaiCompatible: { reasoning_content: "opaque reasoning" } },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
reasoning_content: "opaque reasoning",
|
||||
tool_calls: undefined,
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("maps OpenAI provider options to Chat options", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
|
||||
@@ -129,62 +129,25 @@ function normalizeMessages(
|
||||
}
|
||||
})
|
||||
|
||||
// Anthropic rejects messages with empty content - filter out empty string messages
|
||||
// and remove empty text/reasoning parts from array content
|
||||
if (model.api.npm === "@ai-sdk/anthropic") {
|
||||
msgs = msgs
|
||||
.map((msg) => {
|
||||
if (typeof msg.content === "string") {
|
||||
if (msg.content === "") return undefined
|
||||
return msg
|
||||
}
|
||||
if (!Array.isArray(msg.content)) return msg
|
||||
const filtered = msg.content.filter((part) => {
|
||||
if (part.type === "text") {
|
||||
return part.text !== ""
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
return (
|
||||
part.text.trim().length > 0 ||
|
||||
part.providerOptions?.anthropic?.signature != null ||
|
||||
part.providerOptions?.anthropic?.redactedData != null
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (filtered.length === 0) return undefined
|
||||
return { ...msg, content: filtered }
|
||||
// Empty assistant turns carry no history and are rejected by several provider APIs.
|
||||
msgs = msgs
|
||||
.map((msg) => {
|
||||
if (msg.role !== "assistant") return msg
|
||||
if (typeof msg.content === "string") return msg.content !== "" ? msg : undefined
|
||||
if (!Array.isArray(msg.content)) return msg
|
||||
const content = msg.content.filter((part) => {
|
||||
if (part.type === "text") return part.text !== ""
|
||||
if (part.type !== "reasoning") return true
|
||||
return (
|
||||
part.text.trim().length > 0 ||
|
||||
(part.providerOptions !== undefined && Object.keys(part.providerOptions).length > 0)
|
||||
)
|
||||
})
|
||||
.filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
|
||||
}
|
||||
|
||||
// Bedrock specific transforms
|
||||
if (model.api.npm === "@ai-sdk/amazon-bedrock") {
|
||||
msgs = msgs
|
||||
.map((msg) => {
|
||||
if (typeof msg.content === "string") {
|
||||
if (msg.content === "") return undefined
|
||||
return msg
|
||||
}
|
||||
if (!Array.isArray(msg.content)) return msg
|
||||
const filtered = msg.content.filter((part) => {
|
||||
if (part.type === "text") {
|
||||
return part.text !== ""
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
return (
|
||||
part.text.trim().length > 0 ||
|
||||
part.providerOptions?.bedrock?.signature != null ||
|
||||
part.providerOptions?.bedrock?.redactedData != null
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (filtered.length === 0) return undefined
|
||||
return { ...msg, content: filtered }
|
||||
})
|
||||
.filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
|
||||
}
|
||||
if (content.length === 0) return undefined
|
||||
if (content.length === msg.content.length) return msg
|
||||
return { ...msg, content }
|
||||
})
|
||||
.filter((msg): msg is ModelMessage => msg !== undefined)
|
||||
|
||||
if (model.api.id.includes("claude")) {
|
||||
const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
|
||||
@@ -255,16 +255,12 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
||||
if (msg.info.role === "assistant") {
|
||||
const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
|
||||
const media: Array<{ mime: string; url: string; filename?: string }> = []
|
||||
const hasSignedReasoning = msg.parts.some((part) => {
|
||||
if (part.type !== "reasoning") return false
|
||||
return part.metadata?.anthropic?.signature != null
|
||||
})
|
||||
|
||||
if (
|
||||
msg.info.error &&
|
||||
!(
|
||||
AbortedError.isInstance(msg.info.error) &&
|
||||
msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (msg.info.error && !AbortedError.isInstance(msg.info.error)) continue
|
||||
const assistantMessage: UIMessage = {
|
||||
id: msg.info.id,
|
||||
role: "assistant",
|
||||
@@ -281,10 +277,6 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
||||
// here is the only safe replay point we have.
|
||||
// Use a single space so the separator survives replay without changing
|
||||
// the neighboring signed reasoning blocks.
|
||||
const hasSignedReasoning = msg.parts.some((part) => {
|
||||
if (part.type !== "reasoning") return false
|
||||
return part.metadata?.anthropic?.signature != null
|
||||
})
|
||||
for (const part of msg.parts) {
|
||||
if (part.type === "text") {
|
||||
const text = part.text === "" && hasSignedReasoning ? " " : part.text
|
||||
|
||||
@@ -950,6 +950,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
}
|
||||
ctx.assistantMessage.error = error
|
||||
ctx.assistantMessage.finish ??= "error"
|
||||
yield* events.publish(Session.Event.Error, {
|
||||
sessionID: ctx.assistantMessage.sessionID,
|
||||
error: ctx.assistantMessage.error,
|
||||
|
||||
@@ -1259,6 +1259,7 @@ export const layer = Layer.effect(
|
||||
providerID: msg.providerID,
|
||||
aborted: true,
|
||||
})
|
||||
msg.finish = "error"
|
||||
msg.time.completed = Date.now()
|
||||
yield* sessions.updateMessage(msg)
|
||||
})
|
||||
|
||||
@@ -2018,7 +2018,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
|
||||
expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" })
|
||||
})
|
||||
|
||||
test("does not filter for non-anthropic providers", () => {
|
||||
test("filters empty assistant content for non-anthropic providers", () => {
|
||||
const openaiModel = {
|
||||
...anthropicModel,
|
||||
providerID: "openai",
|
||||
@@ -2039,9 +2039,32 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
|
||||
|
||||
const result = ProviderTransform.message(msgs, openaiModel, {})
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].content).toBe("")
|
||||
expect(result[1].content).toHaveLength(1)
|
||||
expect(result).toStrictEqual([])
|
||||
})
|
||||
|
||||
test("preserves metadata-only reasoning for non-anthropic providers", () => {
|
||||
const openaiModel = {
|
||||
...anthropicModel,
|
||||
providerID: "openai",
|
||||
api: { id: "gpt-5", url: "https://api.openai.com", npm: "@ai-sdk/openai" },
|
||||
}
|
||||
const reasoning = {
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerOptions: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted" } },
|
||||
} as const
|
||||
|
||||
expect(
|
||||
ProviderTransform.message([{ role: "assistant", content: [reasoning] }], openaiModel, {})[0]?.content,
|
||||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerOptions: expect.objectContaining({
|
||||
openai: expect.objectContaining({ reasoningEncryptedContent: "encrypted" }),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("leaves valid anthropic assistant tool ordering unchanged", () => {
|
||||
|
||||
@@ -988,7 +988,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||
})
|
||||
|
||||
test("includes aborted assistant messages only when they have non-step-start/reasoning content", async () => {
|
||||
test("preserves aborted assistant partial output", async () => {
|
||||
const assistantID1 = "m-assistant-1"
|
||||
const assistantID2 = "m-assistant-2"
|
||||
|
||||
@@ -1038,6 +1038,70 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
{ type: "text", text: "partial answer" },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "reasoning", text: "thinking", providerOptions: undefined }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("preserves empty aborted assistant turns until the provider boundary", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
const input: SessionV1.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(
|
||||
assistantID,
|
||||
"m-parent",
|
||||
new SessionV1.AbortedError({ message: "aborted" }).toObject() as SessionV1.Assistant["error"],
|
||||
),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "a1"),
|
||||
type: "text",
|
||||
text: "",
|
||||
},
|
||||
] as SessionV1.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
const messages = await MessageV2.toModelMessages(input, model)
|
||||
|
||||
expect(messages).toStrictEqual([{ role: "assistant", content: [{ type: "text", text: "" }] }])
|
||||
expect(ProviderTransform.message(messages, model, {})).toStrictEqual([])
|
||||
})
|
||||
|
||||
test("preserves aborted assistant messages with signed reasoning", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
const input: SessionV1.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(
|
||||
assistantID,
|
||||
"m-parent",
|
||||
new SessionV1.AbortedError({ message: "aborted" }).toObject() as SessionV1.Assistant["error"],
|
||||
),
|
||||
parts: [
|
||||
{
|
||||
...basePart(assistantID, "a1"),
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
time: { start: 0 },
|
||||
metadata: { anthropic: { signature: "sig_1" } },
|
||||
},
|
||||
] as SessionV1.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerOptions: { anthropic: { signature: "sig_1" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -1292,6 +1356,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
expect(result).toHaveLength(2)
|
||||
expect((result[0].content as any[]).find((p) => p.type === "text").text).toBe(" ")
|
||||
expect((result[1].content as any[]).find((p) => p.type === "text").text).toBe("the answer")
|
||||
expect(ProviderTransform.message(result, model, {})).toEqual(result)
|
||||
})
|
||||
|
||||
test("leaves empty text alone when reasoning signature is under 'bedrock' namespace", async () => {
|
||||
|
||||
@@ -899,9 +899,13 @@ it.live("session.processor effect tests record aborted errors and idle state", (
|
||||
expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true)
|
||||
}
|
||||
expect(handle.message.error?.name).toBe("MessageAbortedError")
|
||||
expect(handle.message.finish).toBe("end_turn")
|
||||
expect(handle.message.time.completed).toBeNumber()
|
||||
expect(stored.info.role).toBe("assistant")
|
||||
if (stored.info.role === "assistant") {
|
||||
expect(stored.info.error?.name).toBe("MessageAbortedError")
|
||||
expect(stored.info.finish).toBe("end_turn")
|
||||
expect(stored.info.time.completed).toBeNumber()
|
||||
}
|
||||
expect(state).toMatchObject({ type: "idle" })
|
||||
expect(errs).toContain("MessageAbortedError")
|
||||
@@ -957,9 +961,13 @@ it.live("session.processor effect tests mark interruptions aborted without manua
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
expect(handle.message.error?.name).toBe("MessageAbortedError")
|
||||
expect(handle.message.finish).toBe("end_turn")
|
||||
expect(handle.message.time.completed).toBeNumber()
|
||||
expect(stored.info.role).toBe("assistant")
|
||||
if (stored.info.role === "assistant") {
|
||||
expect(stored.info.error?.name).toBe("MessageAbortedError")
|
||||
expect(stored.info.finish).toBe("end_turn")
|
||||
expect(stored.info.time.completed).toBeNumber()
|
||||
}
|
||||
expect(state).toMatchObject({ type: "idle" })
|
||||
}),
|
||||
|
||||
@@ -1082,7 +1082,7 @@ raceNoLLMServer.instance(
|
||||
expect(firstInterrupted?.info.role).toBe("assistant")
|
||||
expect(firstInterrupted?.parts).toHaveLength(0)
|
||||
if (firstInterrupted?.info.role === "assistant") {
|
||||
expect(firstInterrupted.info.finish).toBeUndefined()
|
||||
expect(firstInterrupted.info.finish).toBe("error")
|
||||
expect(firstInterrupted.info.time.completed).toBeNumber()
|
||||
expect(firstInterrupted.info.error?.name).toBe("MessageAbortedError")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user