diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index b3dff297e2..a044aa8ac8 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -547,7 +547,7 @@ Input delivered to a hosted agent-service detached execution callback. | `buildAgentCallContext` | Builds the layered system-message set for one provider call (RFC 0001). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/call-context.ts#L648) | | `buildAgentDelegateTools` | Builds the opt-in delegate tools for a coordinator agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/agent-delegation.ts#L158) | | `buildAgentRunTraceAttributes` | Builds agent run trace attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/trace-attributes.ts#L181) | -| `buildAgUiBrowserFinalizeResponse` | Response payload for build AG-UI browser finalize. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L416) | +| `buildAgUiBrowserFinalizeResponse` | Response payload for build AG-UI browser finalize. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L429) | | `buildAgUiSseTraceSignature` | Build a compact ordered event-type signature for regression checks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/sse-parser.ts#L75) | | `buildChatStreamChunkMessageMetadata` | Builds chat stream chunk message metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/chat-ui-message-helpers.ts#L325) | | `buildChildRunExecutionSnapshot` | Builds child run execution snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/child-run/execution-snapshot.ts#L79) | @@ -634,7 +634,7 @@ Input delivered to a hosted agent-service detached execution callback. | `createAgentServiceRuntime` | Create agent service runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/runtime.ts#L233) | | `createAgentServiceServerRuntime` | Create agent service server runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/server.ts#L54) | | `createAgUiBrowserChunkEncoder` | Create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L103) | -| `createAgUiBrowserEncoderState` | State for create AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L90) | +| `createAgUiBrowserEncoderState` | State for create AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L102) | | `createAgUiBrowserFinalizeTracker` | Create AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L23) | | `createAgUiBrowserResponseStream` | Create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L68) | | `createAgUiCancelHandler` | Handler for create AG-UI cancel. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/run-control.ts#L148) | @@ -772,7 +772,7 @@ Input delivered to a hosted agent-service detached execution callback. | `fetchLatestConversationUserText` | Fetch latest conversation user text helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/artifacts/default-research-artifact-support.ts#L112) | | `filterAgentTraceAttributes` | Filter agent trace attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/trace-attributes.ts#L61) | | `filterHostedChatRuntimeLocalTools` | Filter hosted chat runtime local tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-runtime-tool-assembly.ts#L223) | -| `finalizeAgUiBrowserEvents` | Finalize AG-UI browser events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L984) | +| `finalizeAgUiBrowserEvents` | Finalize AG-UI browser events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L1024) | | `finalizeChildRunExecutionResources` | Finalize child run execution resources helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/child-run/execution-cleanup.ts#L28) | | `finalizeConversationAgentRun` | Finalize conversation agent run helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L1326) | | `finalizeHostedChildForkCompletion` | Finalize hosted child fork completion helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-stream-execution.ts#L156) | @@ -865,7 +865,7 @@ Input delivered to a hosted agent-service detached execution callback. | `mapAgUiRuntimeEventToForkParts` | Map AG-UI runtime event to fork parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/fork-runtime-part-mapper.ts#L229) | | `mapFrameworkEventToForkParts` | Handles map framework event to fork parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/fork-runtime-part-mapper.ts#L408) | | `mapHostedStreamPartToChatUiChunks` | Map hosted stream part to chat UI chunks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/hosted-ui-chunk-mapping.ts#L220) | -| `mapRuntimeStreamEventToAgUiBrowserEvents` | Map runtime stream event to AG-UI browser events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L681) | +| `mapRuntimeStreamEventToAgUiBrowserEvents` | Map runtime stream event to AG-UI browser events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L710) | | `mergeToolCallInput` | Input payload for merge tool call. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/tool-input.ts#L112) | | `mergeToolInputDelta` | Merge tool input delta helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/tool-input.ts#L54) | | `mirrorDefaultResearchRunArtifact` | Mirror default research run artifact helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/artifacts/default-research-artifact-support.ts#L299) | @@ -1181,7 +1181,7 @@ Input delivered to a hosted agent-service detached execution callback. | `AgUiBeforeStreamMessageInput` | Input payload for AG-UI before stream message. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/before-stream.ts#L4) | | `AgUiBeforeStreamResult` | Result returned from AG-UI before stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/before-stream.ts#L25) | | `AgUiBrowserChunkEncoder` | Public API contract for AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L15) | -| `AgUiBrowserEncodedEvent` | Event emitted for AG-UI browser encoded. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L84) | +| `AgUiBrowserEncodedEvent` | Event emitted for AG-UI browser encoded. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L96) | | `AgUiBrowserEncoderState` | State for AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L35) | | `AgUiBrowserFinalizeTracker` | Public API contract for AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L9) | | `AgUiBrowserResponseEncoder` | Public API contract for AG-UI browser response encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L41) | diff --git a/src/agent/ag-ui/browser-encoder.test.ts b/src/agent/ag-ui/browser-encoder.test.ts index 7c878e2e42..4b5bd31735 100644 --- a/src/agent/ag-ui/browser-encoder.test.ts +++ b/src/agent/ag-ui/browser-encoder.test.ts @@ -976,3 +976,80 @@ describe("buildAgUiBrowserFinalizeResponse", () => { }]); }); }); + +describe("agent/ag-ui-browser-encoder tool-input lifecycle", () => { + // Raised in review on #3737. A truncated local tool call terminalizes as + // `tool-input-start` (+ any partial deltas) followed by `tool-output-error`. + // Unlike `tool-input-available` and `tool-input-error`, the output-error + // branch never closed the input, so AG-UI clients saw ToolCallStart and + // ToolCallResult with no ToolCallEnd and an input lifecycle left open. + it("closes an open tool input before emitting its output error", () => { + const state = createAgUiBrowserEncoderState({ nowMs: null, epochMs: null }); + + mapRuntimeStreamEventToAgUiBrowserEvents(state, { + type: "tool-input-start", + toolCallId: "truncated-1", + toolName: "search", + }); + const events = mapRuntimeStreamEventToAgUiBrowserEvents(state, { + type: "tool-output-error", + toolCallId: "truncated-1", + errorText: "interrupted", + }); + + const names = events.map((entry) => entry.event); + assertEquals(names, ["ToolCallEnd", "ToolCallResult"]); + assertEquals( + events[0], + { event: "ToolCallEnd", payload: { toolCallId: "truncated-1" } }, + ); + }); + + it("tolerates a state object built without the tracker", () => { + // `AgUiBrowserEncoderState` is re-exported from `veryfront/agent`, so a + // consumer may hold a state object built against the shape this type had + // before `openToolCallIds` existed. A required field would crash on the + // first `tool-input-start`; `reasoningSpanIndex` is optional for the same + // reason. + const state = createAgUiBrowserEncoderState({ nowMs: null, epochMs: null }); + delete (state as { openToolCallIds?: Set }).openToolCallIds; + + mapRuntimeStreamEventToAgUiBrowserEvents(state, { + type: "tool-input-start", + toolCallId: "legacy-1", + toolName: "search", + }); + const events = mapRuntimeStreamEventToAgUiBrowserEvents(state, { + type: "tool-output-error", + toolCallId: "legacy-1", + errorText: "interrupted", + }); + + assertEquals(events.map((entry) => entry.event), ["ToolCallEnd", "ToolCallResult"]); + }); + + it("does not close a tool input that was already completed", () => { + const state = createAgUiBrowserEncoderState({ nowMs: null, epochMs: null }); + + mapRuntimeStreamEventToAgUiBrowserEvents(state, { + type: "tool-input-start", + toolCallId: "settled-1", + toolName: "search", + }); + mapRuntimeStreamEventToAgUiBrowserEvents(state, { + type: "tool-input-available", + toolCallId: "settled-1", + toolName: "search", + input: { q: "veryfront" }, + }); + const events = mapRuntimeStreamEventToAgUiBrowserEvents(state, { + type: "tool-output-error", + toolCallId: "settled-1", + errorText: "upstream 500", + }); + + // Exactly one ToolCallEnd per call: the input-available branch already + // emitted it, so a normal tool failure must not emit a second. + assertEquals(events.map((entry) => entry.event), ["ToolCallResult"]); + }); +}); diff --git a/src/agent/ag-ui/browser-encoder.ts b/src/agent/ag-ui/browser-encoder.ts index 3b874b27f3..f2f83bb536 100644 --- a/src/agent/ag-ui/browser-encoder.ts +++ b/src/agent/ag-ui/browser-encoder.ts @@ -46,6 +46,18 @@ export interface AgUiBrowserEncoderState { activeStepName: string | null; stepCount: number; streamedToolInputIds: Set; + /** + * Tool calls whose `ToolCallStart` has been emitted but not yet closed with + * a `ToolCallEnd`. Distinct from `streamedToolInputIds`, which tracks + * whether any args were streamed, not whether the call is still open. + * + * Optional, and populated lazily, so a state object built against the shape + * this type had before the tracker existed stays valid — the same reason + * `reasoningSpanIndex` above is optional. This type is re-exported from + * `veryfront/agent`, so a required field would crash existing callers on the + * first `tool-input-start`. + */ + openToolCallIds?: Set; sawVisibleOutput: boolean; sawTerminalError: boolean; metadata: AgUiBrowserRunFinishedMetadata; @@ -107,6 +119,7 @@ export function createAgUiBrowserEncoderState( activeStepName: null, stepCount: 0, streamedToolInputIds: new Set(), + openToolCallIds: new Set(), sawVisibleOutput: false, sawTerminalError: false, metadata: {}, @@ -544,6 +557,21 @@ export function buildAgUiBrowserFinalizeResponse( }; } +/** + * Emit the `ToolCallEnd` for a call whose input never reached a terminal + * input event. Returns nothing when the call was already closed, so a normal + * tool failure does not produce a second end. + */ +function closeOpenToolInput( + state: AgUiBrowserEncoderState, + toolCallId: unknown, +): AgUiBrowserEncodedEvent[] { + if (typeof toolCallId !== "string" || toolCallId.length === 0) return []; + if (state.openToolCallIds?.delete(toolCallId) !== true) return []; + state.streamedToolInputIds.delete(toolCallId); + return [{ event: "ToolCallEnd", payload: { toolCallId } }]; +} + function completeToolInput( state: AgUiBrowserEncoderState, event: AgUiRuntimeStreamEvent, @@ -563,6 +591,7 @@ function completeToolInput( if (toolCallId.length > 0) { state.streamedToolInputIds.delete(toolCallId); + state.openToolCallIds?.delete(toolCallId); } events.push({ @@ -855,6 +884,9 @@ function mapRuntimeStreamEventToAgUiBrowserEventsUnstamped( ...closeOpenReasoningEvent(state), ]; state.sawVisibleOutput = true; + if (typeof event.toolCallId === "string" && event.toolCallId.length > 0) { + (state.openToolCallIds ??= new Set()).add(event.toolCallId); + } events.push({ event: "ToolCallStart", payload: { @@ -928,6 +960,14 @@ function mapRuntimeStreamEventToAgUiBrowserEventsUnstamped( return [ ...closeOpenTextEvent(state), ...closeOpenReasoningEvent(state), + // A truncated local tool call terminalizes as `tool-input-start` + // (plus any partial deltas) and then straight to this event, so the + // input is still open. `tool-input-available` and `tool-input-error` + // close it via `completeToolInput`; this branch has to close it too, + // or the client is left with ToolCallStart and ToolCallResult and no + // ToolCallEnd. No synthetic args are emitted: the model never + // committed any, and inventing `{}` would claim it did. + ...closeOpenToolInput(state, event.toolCallId), createToolResultEvent(event.toolCallId, { error: event.errorText }, true), ]; diff --git a/src/agent/runtime/index.ts b/src/agent/runtime/index.ts index 5ce0bf06d2..8b708bb3c2 100644 --- a/src/agent/runtime/index.ts +++ b/src/agent/runtime/index.ts @@ -2648,9 +2648,13 @@ export class AgentRuntime { if (options.announceInput === true) { // An interrupted call never reached `tool-input-end`, so its // `tool-input-start` is still buffered and `inputAnnounced` is false - // — which would suppress the `tool-output-error` below. On the - // declined-recovery path that leaves the client with a reasoning - // block and then nothing at all. + // — which would suppress the `tool-output-error` below. Every + // terminal path passes `announceInput`, because on all of them the + // run stops here and the client would otherwise be left with + // whatever preceded the truncation and then nothing at all. Which + // path declined recovery — exposed reasoning, a spent step budget, a + // second interruption, an exposed sibling — is invisible to the + // user, so it must not decide whether the failure renders (#3737). // // The name is safe to publish here. `tool-call` is what can supersede // a name, and it also sets `inputAvailable`, which fails the guard @@ -2689,9 +2693,14 @@ export class AgentRuntime { await persistToolResult(toolResult); } for (const toolCall of streamedToolCalls) { - await recordIncompleteLocalToolError(toolCall, { - announceInput: declinedRecoveryForExposedReasoning, - }); + // Terminal. Every incomplete local call recorded here is also + // terminalized into history, so announce unconditionally and let the + // wire carry the same failure. `recordIncompleteLocalToolError` + // guards on `providerExecuted`, completeness and a final result, so + // only genuinely truncated local calls are announced, and + // `announceStreamedToolCallInput` is idempotent for any already + // surfaced upstream. + await recordIncompleteLocalToolError(toolCall, { announceInput: true }); } sendSSE(controller, encoder, { type: "step-end" }); break; diff --git a/src/agent/runtime/refresh.test.ts b/src/agent/runtime/refresh.test.ts index ab13dfa1cd..520e6a9543 100644 --- a/src/agent/runtime/refresh.test.ts +++ b/src/agent/runtime/refresh.test.ts @@ -2053,11 +2053,140 @@ describe("agent runtime refresh hooks", () => { body.match(/"type":"tool-output-available"/g)?.length ?? 0, hasFinalResult ? 1 : 0, ); - assertEquals(body.match(/"type":"tool-output-error"/g)?.length ?? 0, 0); + // #3737. Failing closed is about not re-running the batch, not about + // hiding the truncation. The exposed sibling rendered, but the + // interrupted call is terminalized into history here, so it also has to + // reach the wire — one announce and one failure for it, and none for the + // exposed sibling, which is complete and so never enters that branch. + assertEquals( + body.match( + /"type":"tool-input-start","toolCallId":"truncated-agent-after-exposure"/g, + )?.length ?? 0, + 1, + ); + assertEquals( + body.match( + /"type":"tool-output-error","toolCallId":"truncated-agent-after-exposure"/g, + )?.length ?? 0, + 1, + ); + assertEquals(body.match(/"type":"tool-output-error"/g)?.length ?? 0, 1); + assertEquals(body.match(/"type":"tool-input-error"/g)?.length ?? 0, 0); assertEquals(body.includes("Unexpected recovery."), false); } }); + it("surfaces a truncated local tool call when maxSteps exhaustion ends the run", async () => { + // #3737. Recovery is declined here because the step budget is spent, not + // because reasoning was exposed, so the flush #3735 added never runs. The + // run terminalizes the truncated call into history either way, so the wire + // has to carry the same failure — otherwise the stream ends with no text, + // no tool call and no error. + let finishedResponse: AgentResponse | undefined; + let callCount = 0; + const model: ModelRuntime = { + provider: "hosted", + modelId: "hosted/truncated-at-max-steps", + async doGenerate() { + return { + content: [{ type: "text", text: "unused" }], + finishReason: "stop", + usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, + }; + }, + async doStream() { + callCount++; + if (callCount === 1) { + return { + stream: createRuntimeStream([ + { + type: "tool-call", + toolCallId: "issue3737-committed", + toolName: "issue3737_probe", + input: '{"revision":"first"}', + }, + { + type: "finish", + finishReason: "tool-calls", + usage: { inputTokens: 1, outputTokens: 1 }, + }, + ]), + }; + } + return { + stream: createRuntimeStream([ + { + type: "tool-input-start", + id: "issue3737-truncated", + toolName: "issue3737_probe", + }, + { + type: "tool-input-delta", + id: "issue3737-truncated", + delta: '{"revision":"trunc', + }, + { + type: "finish", + finishReason: "tool-calls", + usage: { inputTokens: 1, outputTokens: 1 }, + }, + ]), + }; + }, + }; + const probeTool = tool({ + id: "issue3737_probe", + description: "Apply a revision", + inputSchema: defineSchema((v) => v.object({ revision: v.string() }))(), + execute: async ({ revision }) => ({ revision }), + }); + const assistant = eagerAgent({ + model: "hosted/truncated-at-max-steps", + system: "Exhaust the step budget on a truncated tool call.", + tools: { issue3737_probe: probeTool }, + maxSteps: 2, + resolveModelTransport: async () => ({ model }), + }); + + const body = await (await assistant.stream({ + input: "Apply both revisions", + onFinish: (result) => { + finishedResponse = result; + }, + })).toDataStreamResponse().text(); + + assertEquals(callCount, 2); + // The truncated call must be announced so its failure card has something + // to render against. + assertEquals( + body.match( + /"type":"tool-input-start","toolCallId":"issue3737-truncated"/g, + )?.length ?? 0, + 1, + ); + // Exactly one failure event on the wire for it — never a tool-input-error + // alongside the tool-output-error. + assertEquals( + body.match(/"type":"tool-output-error","toolCallId":"issue3737-truncated"/g)?.length ?? 0, + 1, + ); + assertEquals(body.match(/"type":"tool-input-error"/g)?.length ?? 0, 0); + assertEquals( + body.includes( + 'Stream terminated before tool-call event fired for \\"issue3737_probe\\"', + ), + true, + ); + assertExists(finishedResponse); + assertEquals( + finishedResponse.toolCalls.map((toolCall) => [toolCall.id, toolCall.status]), + [ + ["issue3737-committed", "completed"], + ["issue3737-truncated", "error"], + ], + ); + }); + it("does not report stale text when an ordinary tool-only step exhausts maxSteps", async () => { let callCount = 0; let finishedResponse: AgentResponse | undefined; @@ -2211,7 +2340,31 @@ describe("agent runtime refresh hooks", () => { assertEquals(callCount, 2); assertEquals(toolResults, []); assertEquals(body.match(/Created the Outlook assistant\./g)?.length ?? 0, 1); - assertEquals(body.match(/"type":"tool-output-error"/g)?.length ?? 0, 0); + // #3737. The step-1 placeholder is still off the wire: recovery ran, so it + // is provisional and gets re-asked. The step-2 placeholder is terminal — + // the assertions below show it kept in the assistant message with a + // matching tool-result error and an errored entry in `toolCalls`, so + // holding it back from the wire made the persisted history and the live + // stream disagree. One announce and one failure, for the terminal one only. + assertEquals( + body.match( + /"type":"tool-input-start","toolCallId":"toolu_repeated_placeholder"/g, + )?.length ?? 0, + 1, + ); + assertEquals( + body.match( + /"type":"tool-input-start","toolCallId":"toolu_placeholder_after_text"/g, + )?.length ?? 0, + 0, + ); + assertEquals( + body.match(/"type":"tool-output-error","toolCallId":"toolu_repeated_placeholder"/g)?.length ?? + 0, + 1, + ); + assertEquals(body.match(/"type":"tool-output-error"/g)?.length ?? 0, 1); + assertEquals(body.match(/"type":"tool-input-error"/g)?.length ?? 0, 0); assertEquals(body.includes("Unexpected second recovery."), false); const completedResponse = finishedResponse as AgentResponse | undefined; assertExists(completedResponse); diff --git a/src/internal-agents/ag-ui-sse.test.ts b/src/internal-agents/ag-ui-sse.test.ts index 910c919333..d34358d3bf 100644 --- a/src/internal-agents/ag-ui-sse.test.ts +++ b/src/internal-agents/ag-ui-sse.test.ts @@ -87,10 +87,17 @@ describe("internal-agents/ag-ui-sse", () => { toolCallId: "tool-1", errorText: "boom", }), - [{ - event: "ToolCallResult", - payload: { toolCallId: "tool-1", result: { error: "boom" }, isError: true }, - }], + // `tool-1` opened with `tool-input-start` above and never reached a + // terminal input event, so its input is closed here before the result. + // This expectation previously omitted `ToolCallEnd`, which left the + // client holding an open tool-input lifecycle for the whole run (#3737). + [ + { event: "ToolCallEnd", payload: { toolCallId: "tool-1" } }, + { + event: "ToolCallResult", + payload: { toolCallId: "tool-1", result: { error: "boom" }, isError: true }, + }, + ], ); assertEquals( mapRuntimeEventToAgUi(state, { type: "error", error: "Runtime failed" }),