UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

225 lines (224 loc) • 7.49 kB
//#region src/client/stream/handles/tools.ts /** * Project a runtime handle to the client SDK surface (promise-only * {@link output}, no {@link status} / {@link error} fields). */ function toClientAssembledToolCall(handle) { return { name: handle.name, callId: handle.callId, id: handle.id, namespace: handle.namespace, input: handle.input, args: handle.args, output: handle.outputPromise }; } /** * Parse wire-format tool payloads into structured values. * * Tool events may carry JSON-encoded object strings on the wire; this * helper normalises them to plain objects for consumers. Non-JSON strings * are returned unchanged. */ function parseToolPayload(value) { if (typeof value !== "string") return value; const trimmed = value.trim(); if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value; try { return JSON.parse(trimmed); } catch { return value; } } /** * Skip wrapper `task` tool events scoped to a subagent namespace. * * Deep-agent subagents are discovered from root-level `task` tool calls; * replaying the same dispatch tool inside the worker namespace would * otherwise surface as a spurious entry in `sub.toolCalls`. */ function shouldIgnoreScopedTaskToolEvent(scopeNamespace, event) { const data = event.params.data; return scopeNamespace.length > 0 && event.params.namespace.length === scopeNamespace.length && event.params.namespace.every((segment, index) => segment === scopeNamespace[index]) && "tool_name" in data && data.tool_name === "task"; } function getWireMessageField(message, field) { if (!message || typeof message !== "object" || Array.isArray(message)) return; const record = message; if (field in record) return record[field]; const kwargs = record.kwargs; if (kwargs != null && field in kwargs) return kwargs[field]; const lcKwargs = record.lc_kwargs; if (lcKwargs != null && field in lcKwargs) return lcKwargs[field]; } function isToolMessageLike(value) { if (!value || typeof value !== "object") return false; if (value.type === "tool") return true; if (getWireMessageField(value, "type") === "tool") return true; return typeof getWireMessageField(value, "tool_call_id") === "string" && getWireMessageField(value, "content") !== void 0; } function isCommandLike(value) { return !!value && typeof value === "object" && value.lg_name === "Command"; } function textFromContentBlocks(content) { let out = ""; for (const block of content) { if (!block || typeof block !== "object") continue; const record = block; if (record.type === "text" && typeof record.text === "string") out += record.text; } return out; } /** * Normalise tool-result `content` from a wire ToolMessage into the value * a tool implementation returned (object, string, etc.). */ function parseToolResultContent(content) { if (content == null) return null; if (typeof content === "string") { if (content.trim().length === 0) return null; return parseToolPayload(content); } if (Array.isArray(content)) { const text = textFromContentBlocks(content); if (text.length === 0) return null; return parseToolPayload(text); } if (typeof content === "object") return content; return null; } function parseToolMessageRecord(message) { return parseToolResultContent(getWireMessageField(message, "content")); } function parseCommandToolOutput(command, toolCallId) { const update = command.update; if (update == null || typeof update !== "object" || Array.isArray(update)) return { found: false }; const messages = update.messages; if (!Array.isArray(messages)) return { found: false }; const toolMessages = messages.filter((message) => isToolMessageLike(message)); if (toolMessages.length === 0) return { found: false }; if (toolCallId != null) { for (const message of toolMessages) { if (getWireMessageField(message, "tool_call_id") !== toolCallId) continue; return { found: true, value: parseToolMessageRecord(message) }; } return { found: false }; } if (toolMessages.length === 1) return { found: true, value: parseToolMessageRecord(toolMessages[0]) }; for (let i = toolMessages.length - 1; i >= 0; i -= 1) { const parsed = parseToolMessageRecord(toolMessages[i]); if (parsed != null) return { found: true, value: parsed }; } return { found: true, value: null }; } /** * Parse a `tool-finished` output payload into the tool's return value. * * Wire events often wrap structured tool results in a ToolMessage-shaped * object (`{ type: "tool", content: "..." }`) or a LangGraph * {@link Command} whose `update.messages` carries the ToolMessage. * This unwraps those envelopes, JSON-decodes string content when possible, * and leaves plain strings as-is. Returns `null` when a ToolMessage envelope * is present but its content cannot be normalised. */ function parseToolOutput(value, toolCallId) { const parsed = parseToolPayload(value); if (isCommandLike(parsed)) { const commandOutput = parseCommandToolOutput(parsed, toolCallId); return commandOutput.found ? commandOutput.value : parsed; } if (isToolMessageLike(parsed)) return parseToolResultContent(getWireMessageField(parsed, "content")); return parsed ?? null; } /** * Incrementally assembles `tools` events into mutable tool-call handles. * * Framework consumers store the handle directly; client SDK consumers * should map with {@link toClientAssembledToolCall} before yielding. */ var ToolCallAssembler = class { active = /* @__PURE__ */ new Map(); consume(event) { const data = event.params.data; if (data.event === "tool-started") return this.handleStarted(event, data); if (data.event === "tool-finished") return this.handleFinished(data); if (data.event === "tool-error") return this.handleError(data); } /** * Reject any in-flight tool calls (e.g. on session close). */ failAll(reason) { for (const entry of this.active.values()) { entry.rejectOutput(reason); entry.handle.status = "error"; entry.handle.error = reason.message; } this.active.clear(); } handleStarted(event, data) { let resolveOutput; let rejectOutput; const outputPromise = new Promise((resolve, reject) => { resolveOutput = resolve; rejectOutput = reject; }); outputPromise.catch(() => void 0); const input = parseToolPayload(data.input); const name = data.tool_name; const callId = data.tool_call_id; const handle = { name, callId, id: callId, namespace: [...event.params.namespace], input, args: input, output: null, status: "running", error: void 0, outputPromise }; this.active.set(callId, { handle, resolveOutput, rejectOutput }); return handle; } handleFinished(data) { const entry = this.active.get(data.tool_call_id); if (!entry) return void 0; this.active.delete(data.tool_call_id); const value = parseToolOutput(data.output, data.tool_call_id); entry.resolveOutput(value); entry.handle.output = value; entry.handle.status = "finished"; entry.handle.error = void 0; return entry.handle; } handleError(data) { const entry = this.active.get(data.tool_call_id); if (!entry) return void 0; this.active.delete(data.tool_call_id); entry.rejectOutput(new Error(data.message)); entry.handle.output = null; entry.handle.status = "error"; entry.handle.error = data.message; return entry.handle; } }; //#endregion export { ToolCallAssembler, parseToolOutput, parseToolPayload, shouldIgnoreScopedTaskToolEvent, toClientAssembledToolCall }; //# sourceMappingURL=tools.js.map