{"record":{"id":"b398bc48bd9a9a21","repo":"moeru-ai/airi","slug":"llm-streaming-control-handler-failed","errorCode":null,"errorMessage":"[llm-streaming-control] handler failed","messagePattern":"\\[llm-streaming-control\\] handler failed","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/pipelines-audio/src/llm-streaming-control/controller.ts","lineNumber":380,"sourceCode":"\n          await handler(\n            parsed.payload,\n            signalContext,\n          )\n\n          emit(context, {\n            type: 'call-handler-end',\n            callName: parsed.name,\n          })\n        }\n        catch (error) {\n          emit(context, {\n            type: 'call-handler-error',\n            callName: parsed.name,\n            error,\n          })\n\n          console.warn(\n            '[llm-streaming-control] handler failed',\n            error,\n          )\n        }\n      }\n\n      return true\n    },\n\n    on(manifest, handler) {\n      return registerHandler(\n        {\n          handlers,\n          callManifests,\n        },\n        manifest,\n        handler,\n      )","sourceCodeStart":362,"sourceCodeEnd":398,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/pipelines-audio/src/llm-streaming-control/controller.ts#L362-L398","documentation":"The same llm-streaming-control controller runs registered handlers for parsed 'call' tokens (tool calls requested by the model). Each call handler is awaited inside its own try/catch; a rejection is emitted as a 'call-handler-error' event carrying callName plus the error, logged here, and the stream continues with a 'call-handler-end' for cleanup. The controller does not crash — the named tool call simply produced no result.","triggerScenarios":"A tool-call handler registered via controller.on throwing for a specific call: unvalidated arguments from the model, missing context (no audio runtime, closed channel), or an unhandled promise inside the handler.","commonSituations":"The model emits tool arguments that don't match the handler's expected schema; a handler depending on state that is not yet initialized at first token; network or filesystem side effects failing.","solutions":["Listen for the 'call-handler-error' event and key on callName to find the failing handler fast","Validate the call arguments (e.g. with Valibot) at the handler entry and return a structured error to the model instead of throwing","Keep handlers side-effect-failure-tolerant: catch inside the handler for non-fatal failures and report them as tool results"],"exampleFix":"// before\ncontroller.on(manifest, async (parsed) => {\n  const args = parsed.arguments as { query: string }\n  await runSearch(args.query) // throws if arguments have another shape\n})\n\n// after\ncontroller.on(manifest, async (parsed) => {\n  const parsed_args = ArgsSchema.safeParse(parsed.arguments)\n  if (!parsed_args.success)\n    return { error: 'invalid arguments' }\n  return runSearch(parsed_args.data.query)\n})","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"const ArgsSchema = v.object({ query: v.string() })\nfunction parseCallArgs(parsed: unknown) {\n  return ArgsSchema.safeParse((parsed as { arguments?: unknown })?.arguments)\n}","tryCatchPattern":"try {\n  return await runTool(parsed.arguments)\n}\ncatch (error) {\n  // return the failure to the model instead of rejecting the handler\n  return { error: errorMessageFrom(error) }\n}","preventionTips":["Validate model-produced tool arguments with a schema (Valibot) at handler entry","Listen for 'call-handler-error' with callName to find the failing tool fast","Return structured error results rather than throwing, so streams keep flowing"],"tags":["llm","streaming","tool-call","event-handler"],"backgroundTag":"event-handler-exception","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}