UNPKG

langchain

Version:
1 lines • 12.3 kB
{"version":3,"file":"tool-call.cjs","names":["ToolMessage","StreamChannel"],"sources":["../../../src/agents/transformers/tool-call.ts"],"sourcesContent":["import {\n StreamChannel,\n type NativeStreamTransformer,\n type ProtocolEvent,\n type ToolCallStream,\n type ToolCallStatus,\n type ToolsEventData,\n type Namespace,\n} from \"@langchain/langgraph\";\nimport { ToolMessage } from \"@langchain/core/messages\";\n\ninterface ToolCallProjection {\n toolCalls: AsyncIterable<ToolCallStream>;\n}\n\n/**\n * Returns true when `ns` belongs to the agent's own graph — i.e. it\n * starts with `path` and is at most one level deeper (the agent's\n * internal nodes like `tools`, `model_request`, etc.).\n *\n * Events from subagent subgraphs (two or more levels deeper) are\n * excluded, so `run.toolCalls` / `run.middleware` only show events\n * from the agent itself, not from its subagents.\n */\nfunction isOwnEvent(ns: Namespace, path: Namespace): boolean {\n if (ns.length < path.length || ns.length > path.length + 1) return false;\n for (let i = 0; i < path.length; i += 1) {\n if (ns[i] !== path[i]) return false;\n }\n return true;\n}\n\n/**\n * Detects when a `tool-error` payload is actually a graph interrupt rather\n * than a genuine tool failure.\n *\n * A tool that calls `interrupt()` throws a `GraphInterrupt`, whose message is\n * the JSON-serialized `Interrupt[]` array. Each entry has the LangGraph\n * `Interrupt` shape `{ id, value }`: a stable `id` (a hash of the checkpoint\n * namespace, generated by `interrupt()` and always present during graph\n * execution) plus the `value` passed to `interrupt(...)`. We require BOTH a\n * string `id` and a `value` on every entry — a bare `value` is not a reliable\n * discriminator, since a genuine tool error message can also be a JSON array\n * of `{ value }` records (e.g. a validator emitting\n * `[{ \"value\": \"bad input\", \"message\": \"invalid\" }]`). Keying off the\n * interrupt `id` keeps real tool failures on the error path.\n *\n * An interrupt is control flow that *suspends* the run (the tool re-runs on\n * resume); it is not an error, so the tool call must stay pending rather than\n * have its `output` promise rejected. Any interrupt qualifies regardless of\n * its `value` shape: HITL middleware interrupts (`value.type === \"tool\"`) and\n * raw `interrupt(...)` calls from inside a tool are treated identically —\n * raising an interrupt in a tool must work whether or not\n * `humanInTheLoopMiddleware` is involved.\n */\nfunction isToolInterrupt(message: string): boolean {\n let parsed: unknown;\n try {\n parsed = JSON.parse(message);\n } catch {\n return false;\n }\n if (!Array.isArray(parsed) || parsed.length === 0) return false;\n return parsed.every((entry) => {\n if (entry == null || typeof entry !== \"object\") return false;\n const record = entry as Record<string, unknown>;\n return typeof record.id === \"string\" && \"value\" in record;\n });\n}\n\n/**\n * Detects serialized LangChain `ToolMessage` values that can appear on\n * `tool-finished.output` after crossing a protocol or serialization boundary.\n *\n * @example\n * ```ts\n * {\n * lc: 1,\n * type: \"constructor\",\n * id: [\"langchain_core\", \"messages\", \"ToolMessage\"],\n * kwargs: { content: \"raw tool result\", tool_call_id: \"call_1\" }\n * }\n * ```\n */\nfunction isSerializedToolMessage(\n value: unknown\n): value is { kwargs?: { content?: unknown } } {\n if (value == null || typeof value !== \"object\") return false;\n const record = value as Record<string, unknown>;\n if (record.type !== \"constructor\" || !Array.isArray(record.id)) return false;\n return record.id[record.id.length - 1] === \"ToolMessage\";\n}\n\nfunction normalizeToolOutput(output: unknown): unknown {\n if (ToolMessage.isInstance(output)) {\n return output.content;\n }\n if (isSerializedToolMessage(output)) {\n return output.kwargs?.content;\n }\n return output;\n}\n\n/**\n * Creates a native transformer that correlates `tools` channel events\n * into per-call {@link ToolCallStream} objects.\n *\n * Marked `__native: true` — projection keys land directly on the\n * `GraphRunStream` instance as `run.toolCalls`.\n */\nexport function createToolCallTransformer(\n path: Namespace\n): () => NativeStreamTransformer<ToolCallProjection> {\n return () => {\n const toolCallsLog = StreamChannel.local<ToolCallStream>();\n\n const pendingCalls = new Map<\n string,\n {\n resolveOutput: (v: unknown) => void;\n rejectOutput: (e: unknown) => void;\n resolveStatus: (v: ToolCallStatus) => void;\n resolveError: (v: string | undefined) => void;\n }\n >();\n\n function createToolCallEntry(\n callId: string,\n name: string,\n rawInput: unknown\n ): void {\n if (pendingCalls.has(callId)) return;\n const input =\n typeof rawInput === \"string\" ? JSON.parse(rawInput) : rawInput;\n\n let resolveOutput!: (v: unknown) => void;\n let rejectOutput!: (e: unknown) => void;\n let resolveStatus!: (v: ToolCallStatus) => void;\n let resolveError!: (v: string | undefined) => void;\n\n const output = new Promise<unknown>((res, rej) => {\n resolveOutput = res;\n rejectOutput = rej;\n });\n // Consuming `output` is optional; mark it handled so a failed tool can't\n // crash the process. Callers that await it still see the rejection.\n output.catch(() => {});\n const status = new Promise<ToolCallStatus>((res) => {\n resolveStatus = res;\n });\n const error = new Promise<string | undefined>((res) => {\n resolveError = res;\n });\n\n pendingCalls.set(callId, {\n resolveOutput,\n rejectOutput,\n resolveStatus,\n resolveError,\n });\n\n toolCallsLog.push({\n name,\n callId,\n input,\n output,\n status,\n error,\n } as ToolCallStream);\n }\n\n return {\n __native: true as const,\n\n init: () => ({\n toolCalls: toolCallsLog,\n }),\n\n process(event: ProtocolEvent): boolean {\n /**\n * Only process events that are at the same depth as the agent's graph.\n */\n if (!isOwnEvent(event.params.namespace, path)) return true;\n\n if (event.method === \"messages\") {\n const data = event.params.data as Record<string, unknown>;\n if (data.event === \"content-block-finish\") {\n const cb = (data.contentBlock ?? data.content_block) as\n | Record<string, unknown>\n | undefined;\n if (cb?.type === \"tool_call\") {\n createToolCallEntry(\n String(cb.id ?? \"\"),\n String(cb.name ?? \"\"),\n cb.args ?? cb.input\n );\n }\n }\n }\n\n if (event.method === \"tools\") {\n const data = event.params.data as ToolsEventData;\n const toolCallId = (data as Record<string, unknown>)\n .tool_call_id as string;\n\n if (data.event === \"tool-started\") {\n createToolCallEntry(\n toolCallId,\n ((data as Record<string, unknown>).tool_name as string) ??\n \"unknown\",\n (data as Record<string, unknown>).input\n );\n }\n\n const pending = toolCallId ? pendingCalls.get(toolCallId) : undefined;\n\n if (pending) {\n if (data.event === \"tool-finished\") {\n pending.resolveOutput(\n normalizeToolOutput((data as Record<string, unknown>).output)\n );\n pending.resolveStatus(\"finished\");\n pending.resolveError(undefined);\n pendingCalls.delete(toolCallId);\n } else if (data.event === \"tool-error\") {\n const message =\n ((data as Record<string, unknown>).message as string) ??\n \"unknown error\";\n // An interrupt raised inside a tool (HITL middleware *or* a\n // raw `interrupt()`) surfaces here as a `tool-error` whose\n // message is the serialized interrupt. It is control flow,\n // not a failure: keep the call pending (it re-runs on resume)\n // and never reject `output`, which would otherwise become an\n // unhandled rejection and crash the run.\n if (isToolInterrupt(message)) {\n return true;\n }\n pending.rejectOutput(new Error(message));\n pending.resolveStatus(\"error\");\n pending.resolveError(message);\n pendingCalls.delete(toolCallId);\n }\n }\n }\n\n return true;\n },\n\n finalize(): void {\n for (const pending of pendingCalls.values()) {\n pending.resolveStatus(\"finished\");\n pending.resolveError(undefined);\n pending.resolveOutput(undefined);\n }\n pendingCalls.clear();\n toolCallsLog.close();\n },\n\n fail(err: unknown): void {\n for (const pending of pendingCalls.values()) {\n pending.resolveStatus(\"error\");\n pending.resolveError(\n err instanceof Error ? err.message : String(err)\n );\n pending.rejectOutput(err);\n }\n pendingCalls.clear();\n toolCallsLog.fail(err);\n },\n };\n };\n}\n"],"mappings":"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"}