fix(ai): preserve provider tool input identity

This commit is contained in:
starptech
2026-07-19 00:54:08 +02:00
parent 7840a0d378
commit 0f26246bfd
21 changed files with 833 additions and 74 deletions
@@ -316,7 +316,21 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult
const wireType = serverToolResultType(part.name)
if (!wireType)
return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock
const errorType = `${wireType}_error`
const syntheticErrorCode =
ProviderShared.isRecord(part.result.value) &&
ProviderShared.isRecord(part.result.value.error) &&
part.result.value.error.type === "provider.invalid-output"
? "invalid_tool_input"
: "unavailable"
const content =
part.result.type !== "error" ||
(ProviderShared.isRecord(part.result.value) &&
part.result.value.type === errorType &&
typeof part.result.value.error_code === "string")
? part.result.value
: { type: errorType, error_code: syntheticErrorCode }
return { type: wireType, tool_use_id: part.id, content } satisfies AnthropicServerToolResultBlock
})
const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: MediaPart) {
@@ -703,7 +717,14 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes
providerExecuted: block.type === "server_tool_use",
}),
},
[...events, LLMEvent.toolInputStart({ id: block.id ?? String(event.index), name: block.name ?? "" })],
[
...events,
LLMEvent.toolInputStart({
id: block.id ?? String(event.index),
name: block.name ?? "",
providerExecuted: block.type === "server_tool_use" ? true : undefined,
}),
],
]
}
+19 -3
View File
@@ -441,6 +441,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
if ("functionCall" in part) {
const input = part.functionCall.args
const id = `tool_${nextToolCallId++}`
const providerMetadata = part.thoughtSignature
? googleMetadata({ thoughtSignature: part.thoughtSignature })
: undefined
lifecycle = Lifecycle.reasoningEnd(
lifecycle,
events,
@@ -448,14 +451,27 @@ const step = (state: ParserState, event: GeminiEvent) => {
reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined,
)
lifecycle = Lifecycle.stepStart(lifecycle, events)
if (typeof input === "string") {
events.push(
LLMEvent.toolInputStart({ id, name: part.functionCall.name, providerMetadata }),
LLMEvent.toolInputEnd({ id, name: part.functionCall.name, input, providerMetadata }),
LLMEvent.toolInputError({
id,
name: part.functionCall.name,
raw: input,
message: `Invalid JSON input for ${ADAPTER} tool call ${part.functionCall.name}`,
providerMetadata,
}),
)
hasToolCalls = true
continue
}
events.push(
LLMEvent.toolCall({
id,
name: part.functionCall.name,
input,
providerMetadata: part.thoughtSignature
? googleMetadata({ thoughtSignature: part.thoughtSignature })
: undefined,
providerMetadata,
}),
)
hasToolCalls = true
+15 -4
View File
@@ -820,22 +820,33 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function*
if (item.type === "function_call") {
if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult
const tools = state.tools[item.id]
const existing = state.tools[item.id]
const providerMetadata = openaiMetadata({ itemId: item.id })
const tools = existing
? state.tools
: ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name })
: ToolStream.start(state.tools, item.id, {
id: item.call_id,
name: item.name,
providerMetadata,
})
const result =
item.arguments === undefined
? yield* ToolStream.finish(ADAPTER, tools, item.id)
: yield* ToolStream.finishWithInput(ADAPTER, tools, item.id, item.arguments)
const events: LLMEvent[] = []
const resultEvents = result.events ?? []
const resultEvents = [
...(existing ? [] : [LLMEvent.toolInputStart({ id: item.call_id, name: item.name, providerMetadata })]),
...(result.events ?? []),
]
const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
events.push(...resultEvents)
return [
{
...state,
lifecycle,
hasFunctionCall: resultEvents.some(LLMEvent.is.toolCall) ? true : state.hasFunctionCall,
hasFunctionCall:
state.hasFunctionCall ||
resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)),
tools: result.tools,
},
events,
+16 -3
View File
@@ -9,6 +9,7 @@ import {
type ContentPart,
type LLMRequest,
type MediaPart,
type ProviderMetadata,
type ToolFileContent,
type TextPart,
type ToolResultPart,
@@ -152,7 +153,16 @@ export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate
* input deltas (e.g. zero-arg tools). The error message is uniform across
* routes: `Invalid JSON input for <route> tool call <name>`.
*/
export const parseToolInput = (route: string, name: string, raw: string) =>
export const parseToolInput = (
route: string,
tool: {
readonly id: string
readonly name: string
readonly providerExecuted?: boolean
readonly providerMetadata?: ProviderMetadata
},
raw: string,
) =>
Effect.try({
try: () => decodeJson(raw || "{}"),
catch: () =>
@@ -161,10 +171,13 @@ export const parseToolInput = (route: string, name: string, raw: string) =>
method: "stream",
reason: new InvalidProviderOutputReason({
route,
message: `Invalid JSON input for ${route} tool call ${name}`,
message: `Invalid JSON input for ${route} tool call ${tool.name}`,
raw,
source: "tool-input",
toolName: name,
toolCallID: tool.id,
toolName: tool.name,
providerExecuted: tool.providerExecuted,
providerMetadata: tool.providerMetadata,
}),
}),
})
+25 -5
View File
@@ -53,6 +53,7 @@ const inputStart = (tool: PendingTool) =>
LLMEvent.toolInputStart({
id: tool.id,
name: tool.name,
providerExecuted: tool.providerExecuted ? true : undefined,
providerMetadata: tool.providerMetadata,
})
@@ -63,8 +64,9 @@ const inputDelta = (tool: PendingTool, text: string) =>
text,
})
const toolCall = (route: string, tool: PendingTool, inputOverride?: string) =>
parseToolInput(route, tool.name, inputOverride ?? tool.input).pipe(
const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
const raw = inputOverride ?? tool.input
return parseToolInput(route, tool, raw).pipe(
Effect.map(
(input): ToolCall =>
LLMEvent.toolCall({
@@ -75,7 +77,20 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) =>
providerMetadata: tool.providerMetadata,
}),
),
Effect.match({
onFailure: (error) =>
LLMEvent.toolInputError({
id: tool.id,
name: tool.name,
raw,
message: error.reason.message,
providerExecuted: tool.providerExecuted ? true : undefined,
providerMetadata: tool.providerMetadata,
}),
onSuccess: (event) => event,
}),
)
}
/** Store the updated tool and produce the optional public delta event. */
const appendTool = <K extends StreamKey>(
@@ -158,8 +173,8 @@ export const appendExisting = <K extends StreamKey>(
/**
* Finalize one pending tool call: parse the accumulated raw JSON, remove it
* from state, and return the optional public `tool-call` event. Missing keys are
* a no-op because some providers emit stop events for non-tool content blocks.
* from state, and emit either `tool-call` or `tool-input-error`. Missing keys
* are a no-op because some providers emit stop events for non-tool blocks.
*/
export const finish = <K extends StreamKey>(route: string, tools: State<K>, key: K) =>
Effect.gen(function* () {
@@ -186,7 +201,12 @@ export const finishWithInput = <K extends StreamKey>(route: string, tools: State
return {
tools: withoutTool(tools, key),
events: [
LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }),
LLMEvent.toolInputEnd({
id: tool.id,
name: tool.name,
input,
providerMetadata: tool.providerMetadata,
}),
yield* toolCall(route, tool, input),
],
}
+2
View File
@@ -107,7 +107,9 @@ export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOut
route: Schema.optional(Schema.String),
raw: Schema.optional(Schema.String),
source: Schema.optional(Schema.Literal("tool-input")),
toolCallID: Schema.optional(Schema.String),
toolName: Schema.optional(Schema.String),
providerExecuted: Schema.optional(Schema.Boolean),
providerMetadata: Schema.optional(ProviderMetadata),
}) {}
+20
View File
@@ -129,6 +129,7 @@ export const ToolInputStart = Schema.Struct({
type: Schema.tag("tool-input-start"),
id: ToolCallID,
name: Schema.String,
providerExecuted: Schema.optional(Schema.Boolean),
providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ToolInputStart" })
export type ToolInputStart = Schema.Schema.Type<typeof ToolInputStart>
@@ -145,10 +146,22 @@ export const ToolInputEnd = Schema.Struct({
type: Schema.tag("tool-input-end"),
id: ToolCallID,
name: Schema.String,
input: Schema.optional(Schema.String),
providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ToolInputEnd" })
export type ToolInputEnd = Schema.Schema.Type<typeof ToolInputEnd>
export const ToolInputError = Schema.Struct({
type: Schema.tag("tool-input-error"),
id: ToolCallID,
name: Schema.String,
raw: Schema.String,
message: Schema.String,
providerExecuted: Schema.optional(Schema.Boolean),
providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ToolInputError" })
export type ToolInputError = Schema.Schema.Type<typeof ToolInputError>
export const ToolCall = Schema.Struct({
type: Schema.tag("tool-call"),
id: ToolCallID,
@@ -216,6 +229,7 @@ const llmEventTagged = Schema.Union([
ToolInputStart,
ToolInputDelta,
ToolInputEnd,
ToolInputError,
ToolCall,
ToolResult,
ToolError,
@@ -253,6 +267,8 @@ export const LLMEvent = Object.assign(llmEventTagged, {
toolInputDelta: (input: WithID<ToolInputDelta, ToolCallID>) =>
ToolInputDelta.make({ ...input, id: toolCallID(input.id) }),
toolInputEnd: (input: WithID<ToolInputEnd, ToolCallID>) => ToolInputEnd.make({ ...input, id: toolCallID(input.id) }),
toolInputError: (input: WithID<ToolInputError, ToolCallID>) =>
ToolInputError.make({ ...input, id: toolCallID(input.id) }),
toolCall: (input: WithID<ToolCall, ToolCallID>) => ToolCall.make({ ...input, id: toolCallID(input.id) }),
toolResult: (input: WithID<ToolResult, ToolCallID>) =>
ToolResult.make({
@@ -283,6 +299,7 @@ export const LLMEvent = Object.assign(llmEventTagged, {
toolInputStart: llmEventTagged.guards["tool-input-start"],
toolInputDelta: llmEventTagged.guards["tool-input-delta"],
toolInputEnd: llmEventTagged.guards["tool-input-end"],
toolInputError: llmEventTagged.guards["tool-input-error"],
toolCall: llmEventTagged.guards["tool-call"],
toolResult: llmEventTagged.guards["tool-result"],
toolError: llmEventTagged.guards["tool-error"],
@@ -498,6 +515,7 @@ const reduceToolInputEnd = (state: ResponseState, event: ToolInputEnd): Response
[event.id]: {
...current,
name: event.name,
text: event.input ?? current.text,
providerMetadata: event.providerMetadata ?? current.providerMetadata,
},
},
@@ -548,6 +566,8 @@ const reduceResponseState = (state: ResponseState, event: LLMEvent): ResponseSta
return reduceToolInputDelta(next, event)
case "tool-input-end":
return reduceToolInputEnd(next, event)
case "tool-input-error":
return next
case "tool-call":
return reduceToolCall(next, event)
case "tool-result":
@@ -617,6 +617,43 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("preserves provider execution identity for malformed server tool input", () =>
Effect.gen(function* () {
const body = sseEvents(
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
{
type: "content_block_start",
index: 0,
content_block: { type: "server_tool_use", id: "srvtoolu_malformed", name: "web_search" },
},
{
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: '{"query":"partial' },
},
{ type: "content_block_stop", index: 0 },
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
)
const response = yield* LLMClient.generate(
LLM.updateRequest(request, {
tools: [{ name: "web_search", description: "Web search", inputSchema: { type: "object" } }],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(response.events.find((event) => event.type === "tool-input-start")).toMatchObject({
type: "tool-input-start",
id: "srvtoolu_malformed",
providerExecuted: true,
})
expect(response.events.find((event) => event.type === "tool-input-error")).toMatchObject({
type: "tool-input-error",
id: "srvtoolu_malformed",
raw: '{"query":"partial',
providerExecuted: true,
})
}),
)
it.effect("decodes web_search_tool_result_error as provider-executed error result", () =>
Effect.gen(function* () {
const body = sseEvents(
@@ -708,6 +745,55 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("lowers synthetic server tool failures to valid Anthropic error payloads", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicBody>(
LLM.request({
id: "req_server_tool_input_error",
model,
messages: [
Message.assistant([
{
type: "tool-call",
id: "srvtoolu_malformed",
name: "web_search",
input: {},
providerExecuted: true,
},
{
type: "tool-result",
id: "srvtoolu_malformed",
name: "web_search",
result: {
type: "error",
value: {
error: { type: "provider.invalid-output" },
raw: '{"query":"partial',
},
},
providerExecuted: true,
},
]),
],
}),
)
expect(prepared.body.messages).toMatchObject([
{
role: "assistant",
content: [
{ type: "server_tool_use", id: "srvtoolu_malformed", name: "web_search", input: {} },
{
type: "web_search_tool_result",
tool_use_id: "srvtoolu_malformed",
content: { type: "web_search_tool_result_error", error_code: "invalid_tool_input" },
},
],
},
])
}),
)
it.effect("rejects round-trip for unknown server tool names", () =>
Effect.gen(function* () {
const error = yield* LLMClient.prepare(
@@ -303,6 +303,36 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("emits malformed streamed tool input without a tool call", () =>
Effect.gen(function* () {
const body = eventStreamBody(
["messageStart", { role: "assistant" }],
[
"contentBlockStart",
{
contentBlockIndex: 0,
start: { toolUse: { toolUseId: "tool_malformed", name: "lookup" } },
},
],
["contentBlockDelta", { contentBlockIndex: 0, delta: { toolUse: { input: '{"query":"partial' } } }],
["contentBlockStop", { contentBlockIndex: 0 }],
["messageStop", { stopReason: "tool_use" }],
)
const response = yield* LLMClient.generate(
LLM.updateRequest(baseRequest, {
tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object" } }],
}),
).pipe(Effect.provide(fixedBytes(body)))
expect(response.toolCalls).toEqual([])
expect(response.events.find((event) => event.type === "tool-input-error")).toMatchObject({
type: "tool-input-error",
id: "tool_malformed",
raw: '{"query":"partial',
})
}),
)
it.effect("decodes reasoning deltas", () =>
Effect.gen(function* () {
const body = eventStreamBody(
+29
View File
@@ -490,6 +490,35 @@ describe("Gemini route", () => {
}),
)
it.effect("reports string-encoded function arguments without repairing them", () =>
Effect.gen(function* () {
const body = sseEvents({
candidates: [
{
content: {
role: "model",
parts: [{ functionCall: { name: "lookup", args: '{"query":"partial' } }],
},
finishReason: "STOP",
},
],
})
const response = yield* LLMClient.generate(
LLM.updateRequest(request, {
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(response.toolCalls).toEqual([])
expect(response.events.find((event) => event.type === "tool-input-error")).toMatchObject({
type: "tool-input-error",
id: "tool_0",
name: "lookup",
raw: '{"query":"partial',
})
}),
)
it.effect("assigns unique ids to multiple streamed tool calls", () =>
Effect.gen(function* () {
const body = sseEvents({
@@ -606,6 +606,33 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("preserves a valid parallel call when another call is malformed", () =>
Effect.gen(function* () {
const body = sseEvents(
deltaChunk({
role: "assistant",
tool_calls: [
{ index: 0, id: "call_valid", function: { name: "lookup", arguments: '{"query":"weather"}' } },
{ index: 1, id: "call_malformed", function: { name: "lookup", arguments: '{"query":"partial' } },
],
}),
deltaChunk({}, "tool_calls"),
)
const response = yield* LLMClient.generate(
LLM.updateRequest(request, {
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(
response.events.filter((event) => event.type === "tool-call" || event.type === "tool-input-error"),
).toMatchObject([
{ type: "tool-call", id: "call_valid", input: { query: "weather" } },
{ type: "tool-input-error", id: "call_malformed", raw: '{"query":"partial' },
])
}),
)
it.effect("fails a streamed tool call when the provider ends without a finish reason", () =>
Effect.gen(function* () {
const body = sseEvents(
@@ -1238,6 +1238,7 @@ describe("OpenAI Responses route", () => {
type: "tool-input-end",
id: "call_1",
name: "lookup",
input: '{"query":"weather"}',
providerMetadata: { openai: { itemId: "item_1" } },
},
{
@@ -1259,6 +1260,87 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("emits malformed function input when output_item.done arrives without added", () =>
Effect.gen(function* () {
const body = sseEvents(
{
type: "response.output_item.done",
item: {
type: "function_call",
id: "item_malformed",
call_id: "call_malformed",
name: "lookup",
arguments: '{"query":"partial',
},
},
{ type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } },
)
const response = yield* LLMClient.generate(
LLM.updateRequest(request, {
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(
response.events.filter(
(event) =>
event.type === "tool-input-start" || event.type === "tool-input-end" || event.type === "tool-input-error",
),
).toMatchObject([
{ type: "tool-input-start", id: "call_malformed", name: "lookup" },
{
type: "tool-input-end",
id: "call_malformed",
name: "lookup",
input: '{"query":"partial',
},
{
type: "tool-input-error",
id: "call_malformed",
name: "lookup",
raw: '{"query":"partial',
},
])
}),
)
it.effect("uses malformed final function input instead of valid streamed deltas", () =>
Effect.gen(function* () {
const body = sseEvents(
{
type: "response.output_item.added",
item: { type: "function_call", id: "item_1", call_id: "call_1", name: "lookup", arguments: "" },
},
{ type: "response.function_call_arguments.delta", item_id: "item_1", delta: '{"query":"valid"}' },
{
type: "response.output_item.done",
item: {
type: "function_call",
id: "item_1",
call_id: "call_1",
name: "lookup",
arguments: '{"query":"partial',
},
},
{ type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } },
)
const response = yield* LLMClient.generate(
LLM.updateRequest(request, {
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(response.events.find((event) => event.type === "tool-input-end")).toMatchObject({
type: "tool-input-end",
input: '{"query":"partial',
})
expect(response.events.find((event) => event.type === "tool-input-error")).toMatchObject({
type: "tool-input-error",
raw: '{"query":"partial',
})
}),
)
it.effect("decodes web_search_call as provider-executed tool-call + tool-result", () =>
Effect.gen(function* () {
const item = {
+38 -9
View File
@@ -57,32 +57,61 @@ describe("ToolStream", () => {
expect(finished).toEqual({
tools: {},
events: [
{ type: "tool-input-end", id: "call_1", name: "lookup" },
{ type: "tool-input-end", id: "call_1", name: "lookup", input: '{"query":"final"}' },
{ type: "tool-call", id: "call_1", name: "lookup", input: { query: "final" } },
],
})
}),
)
it.effect("classifies malformed tool input with its raw arguments", () =>
it.effect("emits malformed tool input with stable identity and raw arguments", () =>
Effect.gen(function* () {
const tools = ToolStream.start(ToolStream.empty<number>(), 0, {
id: "call_1",
name: "lookup",
input: '{"query":"partial',
})
const error = yield* ToolStream.finish(ADAPTER, tools, 0).pipe(Effect.flip)
const finished = yield* ToolStream.finish(ADAPTER, tools, 0)
expect(error).toBeInstanceOf(LLMError)
expect(error.reason).toMatchObject({
_tag: "InvalidProviderOutput",
source: "tool-input",
toolName: "lookup",
raw: '{"query":"partial',
expect(finished).toMatchObject({
tools: {},
events: [
{ type: "tool-input-end", id: "call_1", name: "lookup" },
{
type: "tool-input-error",
id: "call_1",
name: "lookup",
raw: '{"query":"partial',
message: "Invalid JSON input for test-route tool call lookup",
},
],
})
}),
)
it.effect("preserves valid sibling calls when one input is malformed", () =>
Effect.gen(function* () {
const first = ToolStream.start(ToolStream.empty<number>(), 0, {
id: "call_valid",
name: "lookup",
input: '{"query":"weather"}',
})
const tools = ToolStream.start(first, 1, {
id: "call_malformed",
name: "lookup",
input: '{"query":"partial',
})
const finished = yield* ToolStream.finishAll(ADAPTER, tools)
expect(
finished.events.filter((event) => event.type === "tool-call" || event.type === "tool-input-error"),
).toMatchObject([
{ type: "tool-call", id: "call_valid", input: { query: "weather" } },
{ type: "tool-input-error", id: "call_malformed", raw: '{"query":"partial' },
])
}),
)
it.effect("preserves providerExecuted and clears all tools", () =>
Effect.gen(function* () {
const first: ToolStream.State<number> = ToolStream.start(ToolStream.empty<number>(), 0, {
+23 -13
View File
@@ -161,6 +161,8 @@ export type SessionMessageProviderState6 = { [x: string]: any }
export type SessionMessageProviderState7 = { [x: string]: any }
export type SessionMessageProviderState8 = { [x: string]: any }
export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: number }
export type ModelCapabilities = { tools: boolean; input: Array<string>; output: Array<string> }
@@ -735,16 +737,6 @@ export type SessionTextEnded = {
data: { sessionID: string; assistantMessageID: string; ordinal: number; text: string }
}
export type SessionToolInputStarted = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.tool.input.started"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; assistantMessageID: string; callID: string; name: string }
}
export type SessionToolInputEnded = {
id: string
created: number
@@ -1366,6 +1358,23 @@ export type SessionReasoningEnded = {
}
}
export type SessionToolInputStarted = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.tool.input.started"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
callID: string
name: string
executed?: boolean
state?: SessionMessageProviderState5
}
}
export type SessionToolCalled = {
id: string
created: number
@@ -1379,7 +1388,7 @@ export type SessionToolCalled = {
callID: string
input: { [x: string]: any }
executed: boolean
state?: SessionMessageProviderState5
state?: SessionMessageProviderState6
}
}
@@ -1395,9 +1404,10 @@ export type SessionToolFailed = {
assistantMessageID: string
callID: string
error: SessionStructuredError
raw?: string
result?: any
executed: boolean
resultState?: SessionMessageProviderState7
resultState?: SessionMessageProviderState8
}
}
@@ -1877,7 +1887,7 @@ export type SessionToolSuccess = {
content: Array<LLMToolContent>
result?: any
executed: boolean
resultState?: SessionMessageProviderState6
resultState?: SessionMessageProviderState7
}
}
+3 -1
View File
@@ -324,6 +324,8 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
type: "tool",
id: event.data.callID,
name: event.data.name,
executed: event.data.executed,
providerState: event.data.state,
time: { created: event.created },
state: SessionMessage.ToolStateStreaming.make({ status: "streaming", input: "" }),
}),
@@ -396,7 +398,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
status: "error",
error: event.data.error,
input: typeof match.state.input === "string" ? {} : match.state.input,
raw: typeof match.state.input === "string" ? match.state.input : undefined,
raw: event.data.raw ?? (typeof match.state.input === "string" ? match.state.input : undefined),
structured: match.state.status === "running" ? match.state.structured : {},
content: match.state.status === "running" ? match.state.content : [],
result: event.data.result,
+64 -12
View File
@@ -28,6 +28,8 @@ import { toSessionError } from "../to-session-error"
import { SessionRunnerRetry } from "./retry"
import { SessionUsage } from "../usage"
const MAX_MALFORMED_TOOL_RETRIES = 4
const layer = Layer.effect(
Service,
Effect.gen(function* () {
@@ -80,6 +82,7 @@ const layer = Layer.effect(
sessionID: SessionSchema.ID,
promotion: SessionPending.Delivery | undefined,
step: number,
recoverMalformedToolInput: boolean,
recoverOverflow?: typeof compaction.compact,
assistantMessageID?: SessionMessage.ID,
) {
@@ -143,7 +146,7 @@ const layer = Layer.effect(
}
}
yield* publish(event)
if (event.type !== "tool-call" || event.providerExecuted) return
if (event.type !== "tool-call" || publisher.isProviderExecuted(event.id)) return
const tool = prepared.resolveToolCall(event.name)
if (tool.type === "reject") {
yield* serialized(publisher.failUnsettledTools(tool.error))
@@ -244,13 +247,35 @@ const layer = Layer.effect(
if (overflowFailure) yield* publish(overflowFailure)
const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined
const malformedToolInput =
llmFailure?.reason._tag === "InvalidProviderOutput" && llmFailure.reason.source === "tool-input"
const recoveredMalformedToolInput = malformedToolInput
llmFailure?.reason._tag === "InvalidProviderOutput" &&
llmFailure.reason.source === "tool-input" &&
llmFailure.reason.toolCallID !== undefined &&
llmFailure.reason.toolName !== undefined &&
llmFailure.reason.raw !== undefined
? {
id: llmFailure.reason.toolCallID,
name: llmFailure.reason.toolName,
raw: llmFailure.reason.raw,
providerExecuted: llmFailure.reason.providerExecuted,
providerMetadata: llmFailure.reason.providerMetadata,
}
: undefined
const failedMalformedToolInput = malformedToolInput
? yield* serialized(
publisher.failUnsettledTools({
type: "provider.invalid-output",
message: "Tool call arguments were malformed JSON and were not executed. Retry with valid JSON.",
}),
publisher.failMalformedToolInput(
{
id: malformedToolInput.id,
name: malformedToolInput.name,
raw: malformedToolInput.raw,
providerExecuted: malformedToolInput.providerExecuted,
providerMetadata: malformedToolInput.providerMetadata,
},
{
type: "provider.invalid-output",
message: "Tool call arguments were malformed JSON and were not executed. Retry with valid JSON.",
},
toSessionError(llmFailure),
),
)
: false
if (llmFailure && !publisher.hasProviderError()) {
@@ -267,6 +292,11 @@ const layer = Layer.effect(
}
// Provider error events only arrive from the stream, so the flag is final here.
const providerFailed = publisher.hasProviderError()
const recoveredMalformedToolInput =
recoverMalformedToolInput &&
!providerFailed &&
!streamInterrupted &&
(failedMalformedToolInput || (stream._tag === "Success" && publisher.hasMalformedToolInput()))
// Settle every owned tool fiber. FiberSet.join returns on the first failure, so retain
// the individual fibers and await all exits before publishing the terminal step event.
@@ -346,6 +376,7 @@ const layer = Layer.effect(
return {
_tag: "Completed",
needsContinuation: needsContinuation || recoveredMalformedToolInput,
malformedToolInput: recoveredMalformedToolInput,
step: currentStep,
} as const
}),
@@ -356,6 +387,7 @@ const layer = Layer.effect(
sessionID: SessionSchema.ID,
promotion: SessionPending.Delivery | undefined,
step: number,
recoverMalformedToolInput: boolean,
) {
// Compaction restarts rebuild the request from compacted history without re-promoting.
// Overflow recovery is one-shot: a post-compaction attempt must not recover another
@@ -366,7 +398,14 @@ const layer = Layer.effect(
let assistantMessageID: SessionMessage.ID | undefined
while (true) {
const attempt = yield* Effect.suspend(() =>
attemptStep(sessionID, currentPromotion, currentStep, recoverOverflow, assistantMessageID),
attemptStep(
sessionID,
currentPromotion,
currentStep,
recoverMalformedToolInput,
recoverOverflow,
assistantMessageID,
),
).pipe(
Effect.tapError((error) =>
error instanceof SessionRunnerRetry.RetryableFailure
@@ -388,7 +427,12 @@ const layer = Layer.effect(
.pipe(Effect.andThen(Effect.fail(error.cause)))
}),
)
if (attempt._tag === "Completed") return { needsContinuation: attempt.needsContinuation, step: attempt.step }
if (attempt._tag === "Completed")
return {
needsContinuation: attempt.needsContinuation,
malformedToolInput: attempt.malformedToolInput,
step: attempt.step,
}
if (attempt._tag === "RestartAfterOverflowCompaction") recoverOverflow = undefined
yield* Effect.yieldNow
currentPromotion = undefined
@@ -446,12 +490,19 @@ const layer = Layer.effect(
while (shouldRun) {
let needsContinuation = true
let step = 1
let malformedToolRetries = 0
// Repeat steps while continuation is needed. A step needs continuation only
// when it recorded local tool calls whose results the model has not yet seen;
// a provider error suppresses it. Pending steers also continue the loop so
// interjections are answered before the session goes idle.
// malformed tool input can also continue within its bounded recovery budget.
// A provider error suppresses continuation. Pending steers also continue the
// loop so interjections are answered before the session goes idle.
while (needsContinuation) {
const result = yield* runStep(input.sessionID, promotion, step)
const result = yield* runStep(
input.sessionID,
promotion,
step,
malformedToolRetries < MAX_MALFORMED_TOOL_RETRIES,
)
// Steer/queue promotion inside runStep has already made the pending input a visible
// user message by this point, so the first-user-message check below is reliable.
if (!titleAttempted.has(input.sessionID)) {
@@ -459,6 +510,7 @@ const layer = Layer.effect(
forkTitle(title.generateForFirstPrompt(yield* getSession(input.sessionID)).pipe(Effect.ignore))
}
needsContinuation = result.needsContinuation
malformedToolRetries = result.malformedToolInput ? malformedToolRetries + 1 : 0
step = result.step + 1
if (needsContinuation) {
yield* runPendingCompaction(input.sessionID)
@@ -53,6 +53,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
called: boolean
settled: boolean
providerExecuted: boolean
rawInput: string
}
>()
let assistantMessageID = input.assistantMessageID
@@ -60,6 +61,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
let stepFailed = false
let providerFailed = false
let retryEvidence = false
let malformedToolInput = false
let stepFailure: SessionError.Error | undefined
let stepSettlement:
| {
@@ -112,12 +114,12 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
if (state !== undefined) current.state = { ...current.state, ...state }
return Effect.succeed(current.ordinal)
})
const end = Effect.fnUntraced(function* (id: string, state?: Record<string, unknown>) {
const end = Effect.fnUntraced(function* (id: string, state?: Record<string, unknown>, value?: string) {
const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`))
yield* ended(
id,
current.values.join(""),
value ?? current.values.join(""),
current.ordinal,
state === undefined ? current.state : { ...current.state, ...state },
)
@@ -175,7 +177,12 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
yield* toolInput.flush()
})
const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
const startToolInput = Effect.fnUntraced(function* (event: {
readonly id: string
readonly name: string
readonly providerExecuted?: boolean
readonly providerMetadata?: ProviderMetadata
}) {
if (tools.has(event.id)) return yield* Effect.die(new Error(`Duplicate tool input start: ${event.id}`))
const assistantMessageID = yield* startAssistant()
tools.set(event.id, {
@@ -183,7 +190,8 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
name: event.name,
called: false,
settled: false,
providerExecuted: false,
providerExecuted: event.providerExecuted === true,
rawInput: "",
})
yield* toolInput.start(event.id)
yield* events.publish(SessionEvent.Tool.Input.Started, {
@@ -191,16 +199,60 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
assistantMessageID,
callID: event.id,
name: event.name,
executed: event.providerExecuted,
state: providerState(event.providerMetadata),
})
})
const endToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
const endToolInput = Effect.fnUntraced(function* (event: {
readonly id: string
readonly name: string
readonly input?: string
}) {
const tool = tools.get(event.id)
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${event.id}`))
if (tool.name !== event.name)
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (!toolInput.has(event.id)) return yield* Effect.die(new Error(`Duplicate tool input end: ${event.id}`))
yield* toolInput.end(event.id)
tool.rawInput = event.input ?? tool.rawInput
yield* toolInput.end(event.id, undefined, event.input)
})
const failMalformedToolInput = Effect.fnUntraced(function* (
event: {
readonly id: string
readonly name: string
readonly raw: string
readonly providerExecuted?: boolean
readonly providerMetadata?: ProviderMetadata
},
error: SessionError.Error,
assistantError: SessionError.Error,
) {
if (!tools.has(event.id)) yield* startToolInput(event)
const tool = tools.get(event.id)
if (!tool || tool.called || tool.settled) return false
if (tool.name !== event.name)
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
tool.providerExecuted = event.providerExecuted === true || tool.providerExecuted
const correctedRaw = !toolInput.has(event.id) && tool.rawInput !== event.raw ? event.raw : undefined
if (toolInput.has(event.id)) {
tool.rawInput = event.raw
yield* toolInput.end(event.id, undefined, event.raw)
}
tool.settled = true
malformedToolInput = true
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: input.sessionID,
assistantMessageID: tool.assistantMessageID,
callID: event.id,
error,
raw: correctedRaw,
executed: tool.providerExecuted,
})
yield* startAssistant()
if (stepFailure === undefined) stepFailure = assistantError
return true
})
const flush = Effect.fn("SessionRunner.flush")(function* () {
@@ -210,11 +262,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
const failTools = Effect.fnUntraced(function* (error: SessionError.Error, mode: "all" | "hosted" | "uncalled") {
let failed = false
for (const [callID, tool] of tools) {
if (
tool.settled ||
(mode === "hosted" && !tool.providerExecuted) ||
(mode === "uncalled" && tool.called)
)
if (tool.settled || (mode === "hosted" && !tool.providerExecuted) || (mode === "uncalled" && tool.called))
continue
tool.settled = true
failed = true
@@ -325,6 +373,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
if (tool.name !== event.name)
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (!toolInput.has(event.id)) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`))
tool.rawInput += event.text
yield* toolInput.append(event.id, event.text)
yield* events.publish(SessionEvent.Tool.Input.Delta, {
sessionID: input.sessionID,
@@ -337,16 +386,30 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
case "tool-input-end":
yield* endToolInput(event)
return
case "tool-input-error":
retryEvidence = true
if (
!(yield* failMalformedToolInput(
event,
{
type: "provider.invalid-output",
message: "Tool call arguments were malformed JSON and were not executed. Retry with valid JSON.",
},
{ type: "provider.invalid-output", message: event.message },
))
)
return yield* Effect.die(new Error(`Malformed tool input after call settlement: ${event.id}`))
return
case "tool-call": {
retryEvidence = true
if (!tools.has(event.id)) yield* startToolInput(event)
const tool = tools.get(event.id)!
if (toolInput.has(event.id)) yield* endToolInput(event)
if (toolInput.has(event.id)) yield* endToolInput({ id: event.id, name: event.name })
if (tool.name !== event.name)
return yield* Effect.die(new Error(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
tool.called = true
tool.providerExecuted = event.providerExecuted === true
tool.providerExecuted = event.providerExecuted === true || tool.providerExecuted
yield* events.publish(SessionEvent.Tool.Called, {
sessionID: input.sessionID,
assistantMessageID: tool.assistantMessageID,
@@ -436,10 +499,13 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
publish,
flush,
failAssistant,
failMalformedToolInput,
publishStepFailure,
failUnsettledTools,
hasProviderError: () => providerFailed,
hasMalformedToolInput: () => malformedToolInput,
hasRetryEvidence: () => retryEvidence,
isProviderExecuted: (callID: string) => tools.get(callID)?.providerExecuted === true,
stepFailure: () => stepFailure,
stepSettlement: () => stepSettlement,
startAssistant,
@@ -110,7 +110,12 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
result:
tool.executed === true && tool.state.result !== undefined
? tool.state.result
: { error: tool.state.error, content: tool.state.content, structured: tool.state.structured },
: {
error: tool.state.error,
content: tool.state.content,
structured: tool.state.structured,
...(tool.state.raw === undefined ? {} : { raw: tool.state.raw }),
},
resultType: "error",
providerExecuted: tool.executed,
providerMetadata,
+217 -2
View File
@@ -4134,8 +4134,9 @@ describe("SessionRunnerLLM", () => {
method: "stream",
reason: new InvalidProviderOutputReason({
message: "Invalid JSON input for tool call echo",
raw: '{"text":"partial',
raw: '{"text":"authoritative',
source: "tool-input",
toolCallID: "call-malformed",
toolName: "echo",
}),
})
@@ -4160,7 +4161,7 @@ describe("SessionRunnerLLM", () => {
state: {
status: "error",
input: {},
raw: '{"text":"partial',
raw: '{"text":"authoritative',
error: {
message: "Tool call arguments were malformed JSON and were not executed. Retry with valid JSON.",
},
@@ -4182,6 +4183,7 @@ describe("SessionRunnerLLM", () => {
error: {
message: "Tool call arguments were malformed JSON and were not executed. Retry with valid JSON.",
},
raw: '{"text":"authoritative',
},
},
},
@@ -4209,6 +4211,219 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("does not recover malformed tool input from an interrupted stream", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Interrupt malformed tool input")
const failure = new LLMError({
module: "test",
method: "stream",
reason: new InvalidProviderOutputReason({
message: "Invalid JSON input for tool call echo",
raw: '{"text":"partial',
source: "tool-input",
toolCallID: "call-malformed-interrupted",
toolName: "echo",
}),
})
responseStream = Stream.fromIterable([
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolInputStart({ id: "call-malformed-interrupted", name: "echo" }),
LLMEvent.toolInputDelta({
id: "call-malformed-interrupted",
name: "echo",
text: '{"text":"partial',
}),
]).pipe(
Stream.concat(
Stream.failCause(
Cause.fromReasons([Cause.makeFailReason(failure), Cause.makeInterruptReason(1)]),
),
),
)
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
expect(requests).toHaveLength(1)
}),
)
it.effect("settles a valid sibling tool before recovering malformed input", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Run one valid and one malformed tool")
toolExecutionGate = yield* Deferred.make<void>()
toolExecutionsStarted = yield* Deferred.make<void>()
toolExecutionsReady = 1
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-valid", name: "echo", input: { text: "valid" } }),
LLMEvent.toolInputStart({ id: "call-malformed-sibling", name: "echo" }),
LLMEvent.toolInputDelta({
id: "call-malformed-sibling",
name: "echo",
text: '{"text":"partial',
}),
LLMEvent.toolInputEnd({ id: "call-malformed-sibling", name: "echo" }),
LLMEvent.toolInputError({
id: "call-malformed-sibling",
name: "echo",
raw: '{"text":"partial',
message: "Invalid JSON input for tool call echo",
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
reply.stop(),
]
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* Deferred.await(toolExecutionsStarted)
yield* Deferred.succeed(toolExecutionGate, undefined)
yield* Fiber.join(run)
toolExecutionGate = undefined
toolExecutionsStarted = undefined
expect(requests).toHaveLength(2)
expect(executions).toEqual(["valid"])
expect(requireAssistant(yield* session.context(sessionID)).content).toMatchObject([
{ id: "call-valid", state: { status: "completed", input: { text: "valid" } } },
{
id: "call-malformed-sibling",
state: { status: "error", raw: '{"text":"partial' },
},
])
}),
)
it.effect("limits malformed tool input recovery to four retries", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Keep calling malformed tools")
responses = Array.from({ length: 5 }, (_, index) => {
const id = `call-malformed-${index}`
return [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolInputStart({ id, name: "echo" }),
LLMEvent.toolInputDelta({ id, name: "echo", text: '{"text":"partial' }),
LLMEvent.toolInputEnd({ id, name: "echo" }),
LLMEvent.toolInputError({
id,
name: "echo",
raw: '{"text":"partial',
message: "Invalid JSON input for tool call echo",
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
]
})
expect(yield* session.resume(sessionID).pipe(Effect.flip)).toMatchObject({
_tag: "Session.StepFailedError",
error: { type: "provider.invalid-output", message: "Invalid JSON input for tool call echo" },
})
expect(requests).toHaveLength(5)
expect(executions).toEqual([])
}),
)
it.effect("replays malformed hosted tool input as provider-executed", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Recover a malformed hosted tool")
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolInputStart({
id: "call-hosted-malformed",
name: "web_search",
providerExecuted: true,
}),
LLMEvent.toolInputDelta({
id: "call-hosted-malformed",
name: "web_search",
text: '{"query":"partial',
}),
LLMEvent.toolInputEnd({ id: "call-hosted-malformed", name: "web_search" }),
LLMEvent.toolInputError({
id: "call-hosted-malformed",
name: "web_search",
raw: '{"query":"partial',
message: "Invalid JSON input for Anthropic Messages tool call web_search",
providerExecuted: true,
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
reply.stop(),
]
yield* session.resume(sessionID)
expect(requests).toHaveLength(2)
expect(requests[1].messages).toMatchObject([
{ role: "user" },
{
role: "assistant",
content: [
{
type: "tool-call",
id: "call-hosted-malformed",
providerExecuted: true,
},
{
type: "tool-result",
id: "call-hosted-malformed",
providerExecuted: true,
},
],
},
])
expect(requireAssistant(yield* session.context(sessionID)).content).toMatchObject([
{ id: "call-hosted-malformed", executed: true, state: { status: "error" } },
])
yield* replaySessionProjection(sessionID)
expect(requireAssistant(yield* session.context(sessionID)).content).toMatchObject([
{ id: "call-hosted-malformed", executed: true, state: { status: "error" } },
])
}),
)
it.effect("preserves provider execution identity when the final tool events omit it", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Keep hosted tool identity")
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolInputStart({ id: "call-hosted-start-only", name: "echo", providerExecuted: true }),
LLMEvent.toolInputDelta({ id: "call-hosted-start-only", name: "echo", text: '{"text":"hosted"}' }),
LLMEvent.toolInputEnd({ id: "call-hosted-start-only", name: "echo" }),
LLMEvent.toolCall({ id: "call-hosted-start-only", name: "echo", input: { text: "hosted" } }),
LLMEvent.toolResult({
id: "call-hosted-start-only",
name: "echo",
result: { type: "json", value: { text: "hosted" } },
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
]
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(executions).toEqual([])
expect(requireAssistant(yield* session.context(sessionID)).content).toMatchObject([
{
id: "call-hosted-start-only",
executed: true,
state: { status: "completed", input: { text: "hosted" } },
},
])
}),
)
it.effect("does not continue automatically after a provider error follows a local tool call", () =>
Effect.gen(function* () {
const session = yield* setup
+3
View File
@@ -371,6 +371,8 @@ export namespace Tool {
schema: {
...ToolBase,
name: Schema.String,
executed: Schema.Boolean.pipe(optional),
state: SessionMessage.ProviderState.pipe(optional),
},
})
export type Started = typeof Started.Type
@@ -443,6 +445,7 @@ export namespace Tool {
schema: {
...ToolBase,
error: SessionError.Error,
raw: Schema.String.pipe(optional),
result: Schema.Unknown.pipe(optional),
executed: Schema.Boolean,
resultState: SessionMessage.ProviderState.pipe(optional),
+26 -6
View File
@@ -1020,6 +1020,8 @@ export type GlobalEvent = {
assistantMessageID: string
callID: string
name: string
executed?: boolean
state?: SessionMessageProviderState
}
}
| {
@@ -1093,6 +1095,7 @@ export type GlobalEvent = {
assistantMessageID: string
callID: string
error: SessionStructuredError
raw?: string
result?: unknown
executed: boolean
resultState?: SessionMessageProviderState
@@ -4117,6 +4120,8 @@ export type SyncEventSessionToolInputStarted = {
assistantMessageID: string
callID: string
name: string
executed?: boolean
state?: SessionMessageProviderState
}
}
}
@@ -4215,6 +4220,7 @@ export type SyncEventSessionToolFailed = {
assistantMessageID: string
callID: string
error: SessionStructuredError
raw?: string
result?: unknown
executed: boolean
resultState?: SessionMessageProviderState
@@ -4639,6 +4645,7 @@ export type SessionMessageToolStateError = {
input: {
[key: string]: unknown
}
raw?: string
content: Array<LlmToolContent>
structured: {
[key: string]: unknown
@@ -5275,6 +5282,8 @@ export type SessionToolInputStarted = {
assistantMessageID: string
callID: string
name: string
executed?: boolean
state?: SessionMessageProviderState
}
}
@@ -5393,6 +5402,7 @@ export type SessionToolFailed = {
assistantMessageID: string
callID: string
error: SessionStructuredError
raw?: string
result?: unknown
executed: boolean
resultState?: SessionMessageProviderState
@@ -7479,6 +7489,8 @@ export type EventSessionToolInputStarted = {
assistantMessageID: string
callID: string
name: string
executed?: boolean
state?: SessionMessageProviderState
}
}
@@ -7558,6 +7570,7 @@ export type EventSessionToolFailed = {
assistantMessageID: string
callID: string
error: SessionStructuredError
raw?: string
result?: unknown
executed: boolean
resultState?: SessionMessageProviderState
@@ -9607,6 +9620,10 @@ export type SessionReasoningEndedV2 = {
}
}
export type SessionMessageProviderState5 = {
[key: string]: unknown
}
export type SessionToolInputStartedV2 = {
id: string
created: number
@@ -9625,6 +9642,8 @@ export type SessionToolInputStartedV2 = {
assistantMessageID: string
callID: string
name: string
executed?: boolean
state?: SessionMessageProviderState5
}
}
@@ -9649,7 +9668,7 @@ export type SessionToolInputEndedV2 = {
}
}
export type SessionMessageProviderState5 = {
export type SessionMessageProviderState6 = {
[key: string]: unknown
}
@@ -9674,7 +9693,7 @@ export type SessionToolCalledV2 = {
[key: string]: unknown
}
executed: boolean
state?: SessionMessageProviderState5
state?: SessionMessageProviderState6
}
}
@@ -9702,7 +9721,7 @@ export type SessionToolProgressV2 = {
}
}
export type SessionMessageProviderState6 = {
export type SessionMessageProviderState7 = {
[key: string]: unknown
}
@@ -9729,11 +9748,11 @@ export type SessionToolSuccessV2 = {
content: Array<LlmToolContent>
result?: unknown
executed: boolean
resultState?: SessionMessageProviderState6
resultState?: SessionMessageProviderState7
}
}
export type SessionMessageProviderState7 = {
export type SessionMessageProviderState8 = {
[key: string]: unknown
}
@@ -9755,9 +9774,10 @@ export type SessionToolFailedV2 = {
assistantMessageID: string
callID: string
error: SessionStructuredError
raw?: string
result?: unknown
executed: boolean
resultState?: SessionMessageProviderState7
resultState?: SessionMessageProviderState8
}
}