fix(core): settle malformed tool input on failure (#36477)
This commit is contained in:
parent
02e2277057
commit
8e657c7db5
2 changed files with 76 additions and 15 deletions
|
|
@ -220,8 +220,31 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
||||||
yield* flushFragments()
|
yield* flushFragments()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
tool.settled = true
|
||||||
|
failed = true
|
||||||
|
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
assistantMessageID: tool.assistantMessageID,
|
||||||
|
callID,
|
||||||
|
error,
|
||||||
|
executed: tool.providerExecuted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return failed
|
||||||
|
})
|
||||||
|
|
||||||
const failAssistant = Effect.fnUntraced(function* (error: SessionError.Error, replace = false) {
|
const failAssistant = Effect.fnUntraced(function* (error: SessionError.Error, replace = false) {
|
||||||
yield* flush()
|
yield* flush()
|
||||||
|
yield* failTools(error, "uncalled")
|
||||||
yield* startAssistant()
|
yield* startAssistant()
|
||||||
if (replace || stepFailure === undefined) stepFailure = error
|
if (replace || stepFailure === undefined) stepFailure = error
|
||||||
})
|
})
|
||||||
|
|
@ -245,20 +268,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
||||||
error: SessionError.Error,
|
error: SessionError.Error,
|
||||||
hostedOnly = false,
|
hostedOnly = false,
|
||||||
) {
|
) {
|
||||||
let failed = false
|
return yield* failTools(error, hostedOnly ? "hosted" : "all")
|
||||||
for (const [callID, tool] of tools) {
|
|
||||||
if (tool.settled || (hostedOnly && !tool.providerExecuted)) continue
|
|
||||||
tool.settled = true
|
|
||||||
failed = true
|
|
||||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
assistantMessageID: tool.assistantMessageID,
|
|
||||||
callID,
|
|
||||||
error,
|
|
||||||
executed: tool.providerExecuted,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return failed
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const assistantMessageIDForTool = (callID: string) => {
|
const assistantMessageIDForTool = (callID: string) => {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
Model,
|
Model,
|
||||||
ToolFailure,
|
ToolFailure,
|
||||||
TransportReason,
|
TransportReason,
|
||||||
|
InvalidProviderOutputReason,
|
||||||
InvalidRequestReason,
|
InvalidRequestReason,
|
||||||
RateLimitReason,
|
RateLimitReason,
|
||||||
type LLMClientShape,
|
type LLMClientShape,
|
||||||
|
|
@ -716,7 +717,18 @@ const verifyPartialFlushOnFailure = (kind: FragmentKind) =>
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
finish: "error",
|
finish: "error",
|
||||||
error: { type: "provider.transport", message: "Provider unavailable" },
|
error: { type: "provider.transport", message: "Provider unavailable" },
|
||||||
content: [fixture.expectedContent],
|
content: [
|
||||||
|
kind === "tool input"
|
||||||
|
? {
|
||||||
|
type: "tool",
|
||||||
|
id: fragmentID(kind, "partial"),
|
||||||
|
state: {
|
||||||
|
status: "error",
|
||||||
|
error: { type: "provider.transport", message: "Provider unavailable" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: fixture.expectedContent,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
expect(requests).toHaveLength(1)
|
expect(requests).toHaveLength(1)
|
||||||
|
|
@ -3876,6 +3888,45 @@ describe("SessionRunnerLLM", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("settles malformed streamed tool input before the provider failure", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const session = yield* setup
|
||||||
|
yield* admit(session, "Call a malformed tool")
|
||||||
|
const failure = new LLMError({
|
||||||
|
module: "test",
|
||||||
|
method: "stream",
|
||||||
|
reason: new InvalidProviderOutputReason({ message: "Invalid JSON input for tool call echo" }),
|
||||||
|
})
|
||||||
|
responseStream = Stream.fromIterable([
|
||||||
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
|
LLMEvent.toolInputStart({ id: "call-malformed", name: "echo" }),
|
||||||
|
LLMEvent.toolInputDelta({ id: "call-malformed", name: "echo", text: '{"text":"partial' }),
|
||||||
|
]).pipe(Stream.concat(Stream.fail(failure)))
|
||||||
|
|
||||||
|
expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure)
|
||||||
|
const assistant = requireAssistant(yield* session.context(sessionID))
|
||||||
|
|
||||||
|
response = reply.stop()
|
||||||
|
yield* admit(session, "Continue")
|
||||||
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
|
expect(yield* recordedStepSettlementEvents(sessionID, assistant.id)).toMatchObject([
|
||||||
|
{ type: "session.step.started.1" },
|
||||||
|
{
|
||||||
|
type: "session.tool.failed.1",
|
||||||
|
data: {
|
||||||
|
callID: "call-malformed",
|
||||||
|
error: { type: "provider.invalid-output", message: "Invalid JSON input for tool call echo" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "session.step.failed.1",
|
||||||
|
data: { error: { type: "provider.invalid-output", message: "Invalid JSON input for tool call echo" } },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("does not continue automatically after a provider error follows a local tool call", () =>
|
it.effect("does not continue automatically after a provider error follows a local tool call", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue