@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
225 lines (224 loc) • 7.49 kB
JavaScript
//#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