Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/ai/src/protocols/utils/tool-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
id: tool.id,
name: tool.name,
raw,
message: error.reason.message,
providerMetadata: tool.providerMetadata,
}),
),
),
Expand Down
4 changes: 1 addition & 3 deletions packages/ai/src/schema/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,12 @@ export const ToolInputEnd = Schema.Struct({
}).annotate({ identifier: "LLM.Event.ToolInputEnd" })
export type ToolInputEnd = Schema.Schema.Type<typeof ToolInputEnd>

/** A local tool call that could not be decoded. `raw` is diagnostic-only. */
/** A local tool call whose final input could not be decoded. */
export const ToolInputError = Schema.Struct({
type: Schema.tag("tool-input-error"),
id: ToolCallID,
name: Schema.String,
raw: Schema.String,
message: Schema.String,
providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ToolInputError" })
export type ToolInputError = Schema.Schema.Type<typeof ToolInputError>

Expand Down
2 changes: 0 additions & 2 deletions packages/ai/test/provider/openai-responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1290,8 +1290,6 @@ describe("OpenAI Responses route", () => {
id: "call_1",
name: "lookup",
raw: '{"query":"partial',
message: "Invalid JSON input for openai-responses tool call lookup",
providerMetadata: { openai: { itemId: "item_1" } },
})
expect(response.finishReason).toBe("tool-calls")
expect(response.events.some(LLMEvent.is.toolCall)).toBeFalse()
Expand Down
1 change: 0 additions & 1 deletion packages/ai/test/response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ describe("LLMResponse reducer", () => {
id: "call_1",
name: "lookup",
raw: '{"query":"partial',
message: "Invalid JSON input",
}),
])

Expand Down
2 changes: 0 additions & 2 deletions packages/ai/test/tool-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ describe("ToolStream", () => {
id: "call_1",
name: "lookup",
raw: '{"query":"partial',
message: "Invalid JSON input for test-route tool call lookup",
},
],
})
Expand Down Expand Up @@ -112,7 +111,6 @@ describe("ToolStream", () => {
id: "call_invalid",
name: "lookup",
raw: '{"query":"partial',
message: "Invalid JSON input for test-route tool call lookup",
},
],
})
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/aisdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,8 +642,6 @@ function streamPartEvents(
id: event.toolCallId,
name: event.toolName,
raw: event.input,
message: error.reason.message,
providerMetadata: providerMetadata(event.providerMetadata),
}),
]),
),
Expand Down
17 changes: 7 additions & 10 deletions packages/core/src/session/runner/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ const layer = Layer.effect(
step: currentStep,
})
}
yield* serialized(publisher.failAssistant(error, true))
yield* serialized(publisher.failAssistant(error))
}
// Provider error events only arrive from the stream, so the flag is final here.
const providerFailed = publisher.hasProviderError()
Expand All @@ -280,7 +280,7 @@ const layer = Layer.effect(
if (settled._tag === "Failure") yield* FiberSet.clear(toolFibers)
if (userDeclined || streamInterrupted || toolsInterrupted) {
yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" }))
yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" }, true))
yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" }))
}
// A settled tool fiber failure is one of two things. A defect from a tool
// implementation becomes a failed tool call the model can read, and the step still
Expand All @@ -293,7 +293,7 @@ const layer = Layer.effect(
const failure = infraError ?? Cause.squash(settledFailure)
const error = toSessionError(failure)
yield* serialized(publisher.failUnsettledTools(error))
if (infraError !== undefined) yield* serialized(publisher.failAssistant(error, true))
if (infraError !== undefined) yield* serialized(publisher.failAssistant(error))
}

// Fail unresolved calls before the terminal step event. Local calls have joined, so
Expand Down Expand Up @@ -321,13 +321,10 @@ const layer = Layer.effect(
: false
if (hostedResultMissing && !publisher.stepSettlement())
yield* serialized(
publisher.failAssistant(
{
type: "tool.result-missing",
message: "Provider did not return a tool result",
},
true,
),
publisher.failAssistant({
type: "tool.result-missing",
message: "Provider did not return a tool result",
}),
)

const stepFailure = publisher.stepFailure()
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/session/runner/publish-llm-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,11 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
return failed
})

const failAssistant = Effect.fnUntraced(function* (error: SessionError.Error, replace = false) {
const failAssistant = Effect.fnUntraced(function* (error: SessionError.Error) {
yield* flush()
yield* failTools(error, "uncalled")
yield* startAssistant()
if (replace || stepFailure === undefined) stepFailure = error
if (stepFailure === undefined) stepFailure = error
})

const publishStepFailure = Effect.fnUntraced(function* (details?: {
Expand Down Expand Up @@ -458,15 +458,15 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
stepSettlement = { finish: event.reason, tokens: SessionUsage.tokens(event.usage) }
if (event.reason === "content-filter") {
providerFailed = true
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" }, true)
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" })
return
}
return
case "finish":
return
case "provider-error":
providerFailed = true
yield* failAssistant({ type: "provider.unknown", message: event.message }, true)
yield* failAssistant({ type: "provider.unknown", message: event.message })
return
}
})
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/session/runner/to-llm-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,10 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid
? [{ type: "text", text: item.text }]
: []
const reuseToolProviderMetadata =
sameModel &&
(message.error === undefined ||
(item.executed === true &&
(item.state.status === "completed" ||
(item.state.status === "error" && item.state.result !== undefined))))
reuseProviderMetadata ||
(sameModel &&
item.executed === true &&
(item.state.status === "completed" || (item.state.status === "error" && item.state.result !== undefined)))
const call = toolCall(
item,
reuseToolProviderMetadata ? providerMetadata(providerMetadataKey, item.providerState) : undefined,
Expand Down
1 change: 0 additions & 1 deletion packages/core/test/aisdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ it.effect("emits malformed AI SDK tool input without executing it", () =>
id: "call_1",
name: "lookup",
raw,
message: "Invalid JSON input for aisdk tool call lookup",
})
expect(response.events.some(LLMEvent.is.toolInputEnd)).toBeTrue()
expect(response.events.some(LLMEvent.is.toolCall)).toBeFalse()
Expand Down
26 changes: 19 additions & 7 deletions packages/core/test/session-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4180,7 +4180,6 @@ describe("SessionRunnerLLM", () => {
id: "call-malformed",
name: "echo",
raw,
message: "Invalid JSON input for test tool call echo",
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
Expand Down Expand Up @@ -4278,7 +4277,6 @@ describe("SessionRunnerLLM", () => {
id: "call-malformed",
name: "echo",
raw: '{"text":"partial',
message: "Invalid JSON input for test tool call echo",
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
Expand Down Expand Up @@ -4321,7 +4319,6 @@ describe("SessionRunnerLLM", () => {
id: "call-malformed",
name: "echo",
raw: '{"text":"partial',
message: "Invalid JSON input for test tool call echo",
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
Expand Down Expand Up @@ -4387,7 +4384,7 @@ describe("SessionRunnerLLM", () => {
}),
)

it.effect("replaces malformed input diagnosis with a later provider failure", () =>
it.effect("records a provider failure after malformed input", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail after malformed input")
Expand All @@ -4402,7 +4399,6 @@ describe("SessionRunnerLLM", () => {
id: "call-malformed",
name: "echo",
raw: '{"text":"partial',
message: "Invalid JSON input for test tool call echo",
}),
]).pipe(Stream.concat(Stream.fail(failure)))

Expand Down Expand Up @@ -4432,7 +4428,6 @@ describe("SessionRunnerLLM", () => {
id,
name: "echo",
raw: '{"text":"partial',
message: "Invalid JSON input for test tool call echo",
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
Expand Down Expand Up @@ -4468,7 +4463,6 @@ describe("SessionRunnerLLM", () => {
id,
name: "echo",
raw: '{"text":"partial',
message: "Invalid JSON input for test tool call echo",
}),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
Expand Down Expand Up @@ -4575,6 +4569,24 @@ describe("SessionRunnerLLM", () => {
}),
)

it.effect("preserves the provider failure when tool output persistence also fails", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Storage fails while provider fails")
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-store-provider-error", name: "storefail", input: {} }),
LLMEvent.providerError({ message: "Provider unavailable" }),
]

expect(yield* session.resume(sessionID).pipe(Effect.exit)).toMatchObject({ _tag: "Failure" })

expect(requireAssistant(yield* session.context(sessionID))).toMatchObject({
error: { type: "provider.unknown", message: "Provider unavailable" },
})
}),
)

it.effect("durably fails a hosted tool left unresolved at normal provider EOF", () =>
Effect.gen(function* () {
const session = yield* setup
Expand Down
Loading