langsmith
Version:
Client library to connect to the LangSmith Observability and Evaluation Platform.
586 lines (585 loc) • 25 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamManager = void 0;
const messages_js_1 = require("./messages.cjs");
const traceable_js_1 = require("../../traceable.cjs");
const usage_js_1 = require("./usage.cjs");
const transcripts_js_1 = require("./transcripts.cjs");
function isRecord(value) {
return typeof value === "object" && value != null && !Array.isArray(value);
}
function isToolResultError(value) {
return isRecord(value) && (value.is_error === true || value.isError === true);
}
function makeSubagentTranscriptPathKey(path, toolUseId, agentType) {
return JSON.stringify([path, toolUseId ?? null, agentType ?? null]);
}
/**
* @internal
*/
class StreamManager {
constructor() {
Object.defineProperty(this, "rootRun", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "namespaces", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "history", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "assistant", {
enumerable: true,
configurable: true,
writable: true,
value: {}
});
Object.defineProperty(this, "tools", {
enumerable: true,
configurable: true,
writable: true,
value: {}
});
Object.defineProperty(this, "subagents", {
enumerable: true,
configurable: true,
writable: true,
value: {}
});
Object.defineProperty(this, "mainTranscriptPath", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "subagentTranscriptPaths", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "pendingAgentTools", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
Object.defineProperty(this, "agentToToolUseId", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
Object.defineProperty(this, "transcriptPathKeys", {
enumerable: true,
configurable: true,
writable: true,
value: new Set()
});
Object.defineProperty(this, "resultModelUsage", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "postRunQueue", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "runTrees", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
const rootRun = (0, traceable_js_1.getCurrentRunTree)(true);
this.rootRun = rootRun;
this.namespaces = rootRun?.createChild ? { root: rootRun } : {};
this.history = { root: [] };
if (rootRun != null) {
StreamManager.managersByRootRun.set(rootRun, this);
}
StreamManager.liveManagers.add(this);
}
dispose() {
StreamManager.liveManagers.delete(this);
if (this.rootRun != null) {
StreamManager.managersByRootRun.delete(this.rootRun);
}
}
async addMessage(message) {
const eventTime = Date.now();
// Short-circuit if no root run found
// This can happen if tracing is disabled globally
if (this.namespaces["root"] == null)
return;
if (message.type === "result") {
if (message.modelUsage) {
this.resultModelUsage = message.modelUsage;
}
const usage = message.modelUsage
? (0, usage_js_1.aggregateUsageFromModelUsage)(message.modelUsage)
: (0, usage_js_1.extractUsageFromMessage)(message);
if (message.total_cost_usd != null && usage != null) {
usage.total_cost = message.total_cost_usd;
}
this.namespaces["root"].extra ??= {};
this.namespaces["root"].extra.metadata ??= {};
this.namespaces["root"].extra.metadata.usage_metadata = usage;
this.namespaces["root"].extra.metadata.is_error = message.is_error;
this.namespaces["root"].extra.metadata.num_turns = message.num_turns;
this.namespaces["root"].extra.metadata.session_id = message.session_id;
this.namespaces["root"].extra.metadata.duration_ms = message.duration_ms;
this.namespaces["root"].extra.metadata.duration_api_ms =
message.duration_api_ms;
}
// Skip non-user / non-assistant messages
if (!("message" in message))
return;
const namespace = (() => {
if ("parent_tool_use_id" in message)
return message.parent_tool_use_id ?? "root";
return "root";
})();
// `eventTime` records the time we receive an event, which for `includePartialMessages: false`
// equals to the end time of an LLM block, so we need to use the first available end time within namespace.
const candidateStartTime = this.namespaces[namespace]?.child_runs?.at(-1)?.end_time ??
this.namespaces[namespace]?.start_time ??
eventTime;
this.history[namespace] ??= this.history["root"].slice();
if (message.type === "assistant") {
const messageId = message.message.id;
this.assistant[messageId] ??= this.createChild(namespace, {
name: "claude.assistant.turn",
run_type: "llm",
start_time: candidateStartTime,
inputs: {
messages: (0, messages_js_1.convertFromAnthropicMessage)(this.history[namespace]),
},
outputs: { output: { messages: [] } },
});
if (this.assistant[messageId] == null)
return;
this.assistant[messageId].outputs = (() => {
const prevMessages = this.assistant[messageId].outputs?.output.messages ?? [];
const newMessages = (0, messages_js_1.convertFromAnthropicMessage)([message]);
return {
output: {
messages: (0, messages_js_1.mergeMessagesById)(prevMessages, newMessages),
},
};
})();
this.assistant[messageId].end_time = eventTime;
this.assistant[messageId].extra ??= {};
this.assistant[messageId].extra.metadata ??= {};
if (message.message.model != null) {
this.assistant[messageId].extra.metadata.ls_model_name =
message.message.model;
}
this.assistant[messageId].extra.metadata.usage_metadata =
(0, usage_js_1.extractUsageFromMessage)(message);
const tools = Array.isArray(message.message.content)
? message.message.content.filter((block) => (0, messages_js_1.isToolBlock)(block))
: [];
for (const block of tools) {
if ((0, messages_js_1.isTaskTool)(block)) {
this.createAgentToolRun(namespace, block, eventTime);
}
else {
this.createToolRun(namespace, block, eventTime);
}
}
}
if (message.type === "user") {
const toolResultBlocks = Array.isArray(message.message.content)
? message.message.content.filter((block) => "tool_use_id" in block)
: [];
const getToolOutput = (result) => {
if (isRecord(result)) {
return result;
}
return { content: result };
};
const getToolError = (result) => {
if (["string", "number", "boolean"].includes(typeof result)) {
return String(result);
}
if (Array.isArray(result)) {
return result.map(getToolError).join("\n");
}
if (isRecord(result)) {
if (typeof result.error === "string")
return result.error;
if (typeof result.text === "string")
return result.text;
if ("content" in result)
return getToolError(result.content);
}
return JSON.stringify(result);
};
for (const block of toolResultBlocks) {
const tool = this.tools[block.tool_use_id];
const subagent = this.subagents[block.tool_use_id];
if (tool != null || subagent != null) {
// Previous versions of @anthropic-ai/claude-agent-sdk did provide
// tool result in `message.tool_use_result`, but at least since 0.2.50 it disappeared,
// so we rely on the last tool result block instead.
const result = message.tool_use_result != null && toolResultBlocks.length === 1
? message.tool_use_result
: block.content;
const toolOutput = getToolOutput(result);
const isError = isToolResultError(block) || isToolResultError(result);
const toolError = isError ? getToolError(result) : undefined;
await tool?.end(toolOutput, toolError);
// Match Python's lifecycle: PostToolUse sets outputs on the
// subagent chain, but the subagent itself is not ended until after
// transcript reconciliation. Hidden transcript LLM/tool children can
// arrive after the Agent/Task tool result, so ending the chain here
// can make reconciled children appear outside their parent bounds.
if (subagent != null) {
subagent.outputs ??= toolOutput;
subagent.error ??= toolError;
}
}
}
}
this.history[namespace].push(message);
}
addHookEvent(input, toolUseId) {
if (typeof input !== "object" || input == null)
return;
const data = input;
if (this.mainTranscriptPath == null &&
typeof data.transcript_path === "string") {
this.mainTranscriptPath = data.transcript_path;
}
if (data.hook_event_name === "PreToolUse" &&
typeof toolUseId === "string" &&
(data.tool_name === "Agent" || data.tool_name === "Task")) {
this.pendingAgentTools.set(toolUseId, typeof data.tool_input === "object" && data.tool_input != null
? data.tool_input
: {});
return;
}
if (data.hook_event_name === "SubagentStart") {
const agentId = typeof data.agent_id === "string" ? data.agent_id : undefined;
if (agentId == null)
return;
const agentType = typeof data.agent_type === "string" ? data.agent_type : undefined;
let matchedToolUseId;
for (const [pendingToolUseId, toolInput] of this.pendingAgentTools) {
const pendingAgentType = typeof toolInput.subagent_type === "string"
? toolInput.subagent_type
: typeof toolInput.agent_type === "string"
? toolInput.agent_type
: undefined;
if (agentType == null ||
pendingAgentType == null ||
pendingAgentType === agentType) {
matchedToolUseId = pendingToolUseId;
break;
}
}
matchedToolUseId ??= this.pendingAgentTools.keys().next().value;
if (matchedToolUseId != null) {
this.agentToToolUseId.set(agentId, matchedToolUseId);
this.pendingAgentTools.delete(matchedToolUseId);
}
return;
}
if (data.hook_event_name === "SubagentStop") {
const transcriptPath = typeof data.agent_transcript_path === "string"
? data.agent_transcript_path
: undefined;
if (!transcriptPath)
return;
const agentId = typeof data.agent_id === "string" ? data.agent_id : undefined;
const agentType = typeof data.agent_type === "string" ? data.agent_type : undefined;
const mappedToolUseId = agentId != null ? this.agentToToolUseId.get(agentId) : undefined;
if (agentId != null)
this.agentToToolUseId.delete(agentId);
this.addSubagentTranscriptPath(transcriptPath, mappedToolUseId, agentType);
}
}
addSubagentTranscriptPath(path, toolUseId, agentType) {
const key = makeSubagentTranscriptPathKey(path, toolUseId, agentType);
if (this.transcriptPathKeys.has(key))
return;
this.transcriptPathKeys.add(key);
this.subagentTranscriptPaths.push({ path, toolUseId, agentType });
}
static getActiveToolRun(toolName, input) {
const currentRun = (0, traceable_js_1.getCurrentRunTree)(true);
const currentManager = currentRun != null
? StreamManager.managersByRootRun.get(currentRun)
: undefined;
const currentRunTree = currentManager?.getActiveToolRun(toolName, input);
if (currentRunTree != null)
return currentRunTree;
// Last resort: the SDK invoked an MCP handler from a detached async context
// that did not inherit the existing LangSmith AsyncLocalStorage. Require
// both tool name and input to match before scanning live managers to avoid
// cross-query attribution.
if (toolName == null || input === undefined)
return undefined;
for (const manager of Array.from(StreamManager.liveManagers).reverse()) {
if (manager === currentManager)
continue;
const runTree = manager.getActiveToolRun(toolName, input);
if (runTree != null)
return runTree;
}
return undefined;
}
getActiveToolRun(toolName, input) {
const toolEntries = Object.values(this.tools).filter((runTree) => runTree != null && runTree.end_time == null && runTree.error == null);
return toolEntries.find((runTree) => {
if (toolName != null) {
const runName = String(runTree.name);
const nameMatches = runName === toolName ||
runName.includes(toolName) ||
toolName.includes(runName);
if (!nameMatches)
return false;
}
if (input !== undefined) {
const recorded = runTree.inputs?.input ?? {};
try {
return JSON.stringify(recorded) === JSON.stringify(input ?? {});
}
catch {
return false;
}
}
return true;
});
}
createChild(namespace, args) {
const parentRunTree = this.namespaces[namespace];
if (parentRunTree == null)
return undefined;
return this.createChildRun(parentRunTree, args);
}
createChildRun(parentRunTree, args) {
const runTree = parentRunTree.createChild(args);
if (runTree == null)
return undefined;
this.postRunQueue.push(runTree.postRun());
this.runTrees.push(runTree);
return runTree;
}
getAgentName(block) {
const subagentType = block.input.subagent_type;
if (typeof subagentType === "string" && subagentType.length > 0) {
return subagentType;
}
const agentType = block.input.agent_type;
if (typeof agentType === "string" && agentType.length > 0) {
return agentType;
}
const description = block.input.description;
if (typeof description === "string" && description.length > 0) {
return description.split(" ")[0] || "unknown-agent";
}
return "unknown-agent";
}
createToolRun(namespace, block, startTime) {
if (typeof block.id !== "string")
return undefined;
const name = typeof block.name === "string" ? block.name : "unknown-tool";
this.tools[block.id] ??=
this.createChild(namespace, {
name,
run_type: "tool",
inputs: block.input ? { input: block.input } : {},
start_time: startTime,
}) ?? this.tools[block.id];
return this.tools[block.id];
}
createAgentToolRun(namespace, block, startTime) {
if (typeof block.id !== "string")
return;
if (typeof block.input !== "object" ||
block.input == null ||
Array.isArray(block.input)) {
return;
}
const input = block.input;
const agentToolRun = this.createToolRun(namespace, block, startTime);
if (agentToolRun == null)
return;
this.subagents[block.id] ??=
this.createChildRun(agentToolRun, {
name: this.getAgentName({ input }),
run_type: "chain",
inputs: input,
start_time: startTime,
extra: {
metadata: {
ls_agent_type: "subagent",
},
},
}) ?? this.subagents[block.id];
this.namespaces[block.id] ??= this.subagents[block.id];
}
resolveSubagentNamespace(agentType) {
const entries = Object.entries(this.namespaces);
if (agentType == null)
return undefined;
return entries.find(([, runTree]) => runTree?.name === agentType)?.[0];
}
createSyntheticAssistantRun(namespace, turn) {
let runTree = this.assistant[turn.messageId];
if (runTree == null) {
runTree = this.createChild(namespace, {
name: "claude.assistant.turn",
run_type: "llm",
start_time: turn.timestamp,
inputs: turn.inputMessages.length > 0 ? { messages: turn.inputMessages } : {},
outputs: {
output: { messages: (0, messages_js_1.convertFromAnthropicMessage)(turn.message) },
},
extra: {
metadata: {
ls_provider: "anthropic",
...(turn.model != null ? { ls_model_name: turn.model } : {}),
...(turn.usageMetadata != null
? { usage_metadata: turn.usageMetadata }
: {}),
},
},
});
if (runTree == null)
return;
this.assistant[turn.messageId] = runTree;
}
else {
runTree.outputs = {
output: { messages: (0, messages_js_1.convertFromAnthropicMessage)(turn.message) },
};
runTree.extra ??= {};
runTree.extra.metadata ??= {};
if (turn.model != null)
runTree.extra.metadata.ls_model_name = turn.model;
if (turn.usageMetadata != null) {
runTree.extra.metadata.usage_metadata = turn.usageMetadata;
}
}
runTree.end_time = turn.timestamp;
const tools = Array.isArray(turn.message.message.content)
? turn.message.message.content.filter((block) => (0, messages_js_1.isToolBlock)(block))
: [];
for (const block of tools) {
if ((0, messages_js_1.isTaskTool)(block)) {
this.createAgentToolRun(namespace, block, turn.timestamp);
}
else {
this.createToolRun(namespace, block, turn.timestamp);
}
}
}
async reconcileTranscripts() {
const usageByMessageId = {};
if (this.mainTranscriptPath != null) {
const transcript = await (0, transcripts_js_1.readTranscript)(this.mainTranscriptPath);
Object.assign(usageByMessageId, transcript.usageByMessageId);
}
for (const transcriptPath of this.subagentTranscriptPaths) {
const namespace = transcriptPath.toolUseId ??
this.resolveSubagentNamespace(transcriptPath.agentType);
if (namespace == null || this.namespaces[namespace] == null)
continue;
const transcript = await (0, transcripts_js_1.readTranscript)(transcriptPath.path);
Object.assign(usageByMessageId, transcript.usageByMessageId);
for (const turn of transcript.turns) {
this.createSyntheticAssistantRun(namespace, turn);
}
for (const toolResult of transcript.toolResults) {
const tool = this.tools[toolResult.toolUseId];
if (tool == null)
continue;
const output = isRecord(toolResult.content)
? toolResult.content
: { content: toolResult.content };
const error = toolResult.isError
? typeof toolResult.content === "string" ||
typeof toolResult.content === "number" ||
typeof toolResult.content === "boolean"
? String(toolResult.content)
: JSON.stringify(toolResult.content)
: undefined;
await tool.end(output, error);
}
}
for (const [messageId, usage] of Object.entries(usageByMessageId)) {
const runTree = this.assistant[messageId];
if (runTree == null)
continue;
runTree.extra ??= {};
runTree.extra.metadata ??= {};
runTree.extra.metadata.usage_metadata = usage;
}
}
async finish() {
try {
await this.reconcileTranscripts();
if (this.resultModelUsage != null) {
(0, usage_js_1.correctUsageFromResults)(this.resultModelUsage, Object.values(this.assistant).filter((runTree) => runTree != null));
}
// Clean up incomplete tools and finalise subagent calls. This mirrors the
// Python integration: Agent/Task tool runs are ended when their tool result
// arrives, while subagent chain runs are finalised only after transcript
// reconciliation so hidden child LLM/tool runs are created first.
for (const tool of Object.values(this.tools)) {
if (tool == null)
continue;
if (tool.outputs == null && tool.error == null) {
await tool.end(undefined, "Run not completed (conversation ended)");
}
}
for (const subagent of Object.values(this.subagents)) {
if (subagent == null)
continue;
if (subagent.end_time == null) {
if (subagent.outputs == null && subagent.error == null) {
await subagent.end(undefined, "Run not completed (conversation ended)");
}
else {
await subagent.end();
}
}
}
// First make sure all the runs are created
await Promise.allSettled(this.postRunQueue);
// Then patch the runs
await Promise.allSettled(this.runTrees.map((runTree) => runTree.patchRun()));
}
finally {
this.dispose();
}
}
}
exports.StreamManager = StreamManager;
Object.defineProperty(StreamManager, "liveManagers", {
enumerable: true,
configurable: true,
writable: true,
value: new Set()
});
Object.defineProperty(StreamManager, "managersByRootRun", {
enumerable: true,
configurable: true,
writable: true,
value: new WeakMap()
});