diff --git a/README.md b/README.md index 796147c..e5cbe35 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ Each integration links to its installation instructions and API coverage. The li | OpenRouter | [`@telemetry-dev/openrouter`](packages/openrouter) | [`telemetry-dev-openrouter`](sdks/python-openrouter) | | Vercel AI SDK | [`@telemetry-dev/ai-sdk`](packages/ai) | | | TanStack AI | [`@telemetry-dev/tanstack-ai`](packages/tanstack-ai) | | +| LangChain and LangGraph | [`@telemetry-dev/langchain`](packages/langchain) | | | LiteLLM | | [`telemetry-dev-litellm`](sdks/python-litellm) | | MCP | [`@telemetry-dev/mcp`](packages/mcp) | | | Eve | [`@telemetry-dev/eve`](packages/eve) | | @@ -160,7 +161,7 @@ Each integration links to its installation instructions and API coverage. The li Some integrations need no package from this repository, because the tool exports OpenTelemetry itself or through a third-party library: Claude Code, Claude Platform, Claude SDK native spans, -Hermes Agent, OpenClaw, CrewAI, LangChain and LangGraph, and Pydantic AI. Their guides are in +Hermes Agent, OpenClaw, CrewAI, LangChain and LangGraph in Python, and Pydantic AI. Their guides are in [Integrations](https://telemetry.dev/docs/integrations). Other languages can use an existing OTLP/HTTP exporter. diff --git a/docs/sdk-conformance.md b/docs/sdk-conformance.md index 3c49005..7d52596 100644 --- a/docs/sdk-conformance.md +++ b/docs/sdk-conformance.md @@ -116,7 +116,8 @@ process-wide bound (TypeScript `batch.maxStreamBytes`, Python `max_stream_bytes` while no other stream holds bytes in it, not even the few bytes a started stream holds before any output, a stream may grow to 16 MiB even past it); output past either bound is not kept, and its span gets `telemetry.dev.capture.truncated = true`; instrumentation scopes `@telemetry-dev/sdk` and `telemetry_dev` (both -classify as framework `otel`). +classify as framework `otel`, except a span with a `langchain.run.id` attribute, which +`@telemetry-dev/langchain` sets and which classifies as framework `langchain`). The SDK allowlists above drop unknown keys in SDK usage objects. Ingest reports `unrecognized_usage_attribute` for unknown numeric `gen_ai.usage.*`, `ai.usage.*`, and diff --git a/packages/langchain/LICENSE b/packages/langchain/LICENSE new file mode 100644 index 0000000..8592a25 --- /dev/null +++ b/packages/langchain/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 telemetry.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/langchain/README.md b/packages/langchain/README.md new file mode 100644 index 0000000..6e52fe8 --- /dev/null +++ b/packages/langchain/README.md @@ -0,0 +1,85 @@ +# @telemetry-dev/langchain + +LangChain.js and LangGraph.js instrumentation for telemetry.dev. A LangChain callback handler records each run as an OpenTelemetry GenAI span through `@telemetry-dev/sdk`. + +Docs: https://telemetry.dev/docs/integrations/langchain-langgraph + +## Install + +```sh +pnpm add @telemetry-dev/sdk @telemetry-dev/langchain @langchain/core +``` + +Requires `@langchain/core` 1.x and `@telemetry-dev/sdk`, which needs Node.js 20.19.0 or later. + +Set `TELEMETRY_DEV_API_KEY` for telemetry export. + +## Trace a graph or a chain + +Call `init()` once, then give the handler to the run. LangChain hands the handler down to each child run: graph nodes, chains, model calls, tools, and retrievers. + +```ts +import { init } from "@telemetry-dev/sdk"; +import { TelemetryDevCallbackHandler } from "@telemetry-dev/langchain"; + +init({ serviceName: "support-agent" }); + +const agent = graph.compile().withConfig({ + callbacks: [new TelemetryDevCallbackHandler()], +}); + +await agent.invoke( + { messages: [{ role: "user", content: "Where is order 48213?" }] }, + { configurable: { thread_id: "thread-1" }, metadata: { userId: "user-1" } }, +); +``` + +`withConfig()` attaches the handler to each run of `agent`. To trace only one run, pass the handler in the config of that run: `agent.invoke(input, { callbacks: [handler] })`. + +In a short script, call `await shutdown()` from `@telemetry-dev/sdk` before the process exits. Otherwise the last spans can stay in the export queue. + +The handler records nothing until `init()` runs with an API key. + +## Spans + +| LangChain run | Span name | `gen_ai.operation.name` | +| --------------------------- | --------------------------- | ----------------------- | +| LangGraph graph | `invoke_agent ` | `invoke_agent` | +| Graph node, chain, runnable | the run name | `function` | +| Chat model call | `chat ` | `chat` | +| Text completion model call | `text_completion ` | `text_completion` | +| Tool | `execute_tool ` | `execute_tool` | +| Retriever | the retriever name | `function` | + +The handler does not record runs that LangChain tags `langsmith:hidden`, such as the `__start__` step of LangGraph. Their child runs go under the nearest recorded run. + +Each span has `langchain.run.id`, the id of its LangChain run. telemetry.dev uses this attribute to show the span as a LangChain span. + +A graph span records the input of the graph. If the state of the graph has a `messages` list, the output of the graph span is the last message, which is the answer of the agent. Otherwise the output is the full state. The node, model, and tool spans record each step. + +Model spans record the request model, the provider (from `ls_provider`), temperature, maximum tokens, top-p, stop sequences, the input and output messages, the response model, response id, finish reason, and token usage from `usage_metadata`. Input token counts include cache reads and cache writes. telemetry.dev calculates the cost from the token usage. + +Model spans also record the type and name of each tool bound to the model in `gen_ai.tool.definitions`. This attribute does not include the descriptions or schemas of the tools, and the capture options do not change it. + +Streamed model calls also record the time to the first chunk and the time between chunks. + +## Sessions, users, and metadata + +The handler reads these keys from the metadata of a run: + +- `thread_id`, `sessionId`, `session_id`, or `conversation_id` sets `gen_ai.conversation.id`. LangGraph copies `configurable.thread_id` into the metadata of each run. +- `userId` or `user_id` sets `user.id`. + +The two attributes go on each span of the run. The other metadata keys of the app go on the top span as `td.metadata.`. The handler ignores the keys that LangChain and LangGraph add, such as `ls_*`, `langgraph_*`, and `checkpoint_ns`. + +`propagateAttributes()` from `@telemetry-dev/sdk` also operates. Explicit metadata keys on the run take priority. + +## Nesting + +When a run starts in an active span of the app, for example in `startActiveSpan()` or `observe()` from `@telemetry-dev/sdk`, the top span of the run goes under that span. + +The handler sets `awaitHandlers`. Thus LangChain calls the handler before it continues the run, and each span starts in the context of the caller. The handler does only in-memory work, and the SDK exports the spans in the background. + +## Capture + +The `captureInput`, `captureOutput`, `mask`, and `maxAttributeLength` options of `init()` apply to the content that the handler records. With capture off, spans keep the names, model, usage, and errors. The tool names, the run names, and the metadata of the app are still sent. diff --git a/packages/langchain/package.json b/packages/langchain/package.json new file mode 100644 index 0000000..14e670e --- /dev/null +++ b/packages/langchain/package.json @@ -0,0 +1,70 @@ +{ + "name": "@telemetry-dev/langchain", + "version": "0.1.0", + "description": "LangChain.js and LangGraph.js instrumentation for telemetry.dev: a callback handler that records chains, graph nodes, model calls, tools, and retrievers as OpenTelemetry GenAI spans.", + "keywords": [ + "genai", + "langchain", + "langgraph", + "llm", + "observability", + "opentelemetry", + "telemetry", + "tracing" + ], + "homepage": "https://telemetry.dev", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/telemetry-dev/telemetry.git", + "directory": "packages/langchain" + }, + "files": [ + "dist", + "src" + ], + "type": "module", + "exports": { + ".": { + "bun": "./src/index.ts", + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + } + }, + "scripts": { + "build": "pnpm exec vp pack", + "dev": "pnpm exec vp pack --watch", + "test": "vp test", + "check": "vp check", + "prepublishOnly": "vp run build" + }, + "devDependencies": { + "@langchain/core": "catalog:", + "@langchain/langgraph": "catalog:", + "@opentelemetry/api": "catalog:", + "@opentelemetry/sdk-trace-base": "catalog:", + "@telemetry-dev/sdk": "workspace:*", + "@types/node": "catalog:", + "typescript": "catalog:", + "vite-plus": "catalog:", + "vitest": "catalog:", + "zod": "catalog:" + }, + "peerDependencies": { + "@langchain/core": ">=1.0.0 <2", + "@telemetry-dev/sdk": "workspace:^" + } +} diff --git a/packages/langchain/src/generation.ts b/packages/langchain/src/generation.ts new file mode 100644 index 0000000..c9509df --- /dev/null +++ b/packages/langchain/src/generation.ts @@ -0,0 +1,192 @@ +import { + type BaseMessage, + isAIMessage, + isBaseMessage, + isToolMessage, +} from "@langchain/core/messages"; +import type { UsageMetadata } from "@langchain/core/messages"; +import type { LLMResult } from "@langchain/core/outputs"; +import type { GenerationFields, SpanFields, TokenUsage } from "@telemetry-dev/sdk"; + +import { plain, plainMessage } from "./plain.ts"; + +type Metadata = Record; + +// LangChain `ls_provider` values whose OpenTelemetry provider name is different. +const PROVIDERS: Record = { + amazon_bedrock: "aws.bedrock", + azure: "azure.ai.openai", + fireworks: "fireworks_ai", + google_genai: "gcp.gen_ai", + google_vertexai: "gcp.vertex_ai", + mistralai: "mistral_ai", + together: "together_ai", +}; + +/** + * The request fields of a model run. LangChain puts the model, provider, and main sampling + * parameters in the `ls_*` metadata of each chat model, and the full request in + * `invocation_params`. + */ +export function generationRequest( + extraParams: Metadata | undefined, + metadata: Metadata | undefined, +): GenerationFields & { attributes?: Record } { + const params = record(extraParams?.["invocation_params"]); + const provider = string(metadata?.["ls_provider"]); + const tools = toolDefinitions(params?.["tools"]); + + return { + attributes: tools.length > 0 ? { "gen_ai.tool.definitions": JSON.stringify(tools) } : undefined, + model: + string(metadata?.["ls_model_name"]) ?? + string(params?.["model"]) ?? + string(params?.["model_name"]) ?? + string(params?.["modelName"]), + provider: provider === undefined ? undefined : (PROVIDERS[provider] ?? provider), + temperature: number(metadata?.["ls_temperature"]) ?? number(params?.["temperature"]), + maxTokens: + number(metadata?.["ls_max_tokens"]) ?? + number(params?.["max_tokens"]) ?? + number(params?.["max_completion_tokens"]), + topP: number(params?.["top_p"]) ?? number(params?.["topP"]), + stopSequences: strings(metadata?.["ls_stop"]), + }; +} + +/** The response fields of a model run, from its first generation. */ +export function completionFields(output: LLMResult): SpanFields { + const generation = output.generations[0]?.[0]; + + if (!generation) return {}; + + const message: BaseMessage | undefined = + "message" in generation && isBaseMessage(generation.message) ? generation.message : undefined; + + const ai = message && isAIMessage(message) ? message : undefined; + const response = record(message?.response_metadata); + // A streamed Anthropic reply keeps the model and stop reason of the message here. + const extra = record(message?.additional_kwargs); + const info = record(generation.generationInfo); + const llmOutput = record(output.llmOutput); + + return { + output: message ? [plainMessage(message)] : generation.text, + usage: usageOf(ai?.usage_metadata) ?? legacyUsage(record(llmOutput?.["tokenUsage"])), + responseModel: + string(response?.["model_name"]) ?? + string(response?.["model"]) ?? + string(extra?.["model"]) ?? + string(llmOutput?.["model_name"]), + responseId: string(response?.["id"]) ?? providerId(message?.id), + finishReason: + string(response?.["finish_reason"]) ?? + string(response?.["stop_reason"]) ?? + string(extra?.["stop_reason"]) ?? + string(info?.["finish_reason"]), + }; +} + +/** Tool input arrives as text: JSON for a structured tool. */ +export function toolInput(input: string): unknown { + try { + return JSON.parse(input); + } catch { + return input; + } +} + +/** A tool called with a tool call returns a ToolMessage; record its content. */ +export function toolOutput(output: unknown): unknown { + if (isBaseMessage(output) && isToolMessage(output)) return output.content; + + return plain(output); +} + +/** + * The type and name of each tool in the request, in the provider's format: OpenAI + * `{type, function: {name}}`, Anthropic `{name}` or a server tool `{type, name}`, Gemini + * `{functionDeclarations: [{name}]}`, and Bedrock `{toolSpec: {name}}`. Descriptions and schemas + * stay out, so that the tool list does not depend on content capture. + */ +function toolDefinitions(tools: unknown): Array<{ type: string; name: string }> { + if (!Array.isArray(tools)) return []; + + return tools.flatMap((item: unknown) => { + const tool = record(item); + const declarations = tool?.["functionDeclarations"]; + + if (Array.isArray(declarations)) { + return declarations.flatMap((declaration: unknown) => { + const name = string(record(declaration)?.["name"]); + + return name ? [{ type: "function", name }] : []; + }); + } + + const name = + string(record(tool?.["function"])?.["name"]) ?? + string(record(tool?.["toolSpec"])?.["name"]) ?? + string(tool?.["name"]); + + if (!name) return []; + const type = string(tool?.["type"]); + + return [{ type: type === undefined || type === "custom" ? "function" : type, name }]; + }); +} + +function usageOf(usage: UsageMetadata | undefined): TokenUsage | undefined { + if (!usage) return undefined; + const input = usage.input_token_details; + const output = usage.output_token_details; + + return { + inputTokens: usage.input_tokens, + outputTokens: usage.output_tokens, + totalTokens: usage.total_tokens, + cacheReadInputTokens: input?.cache_read, + cacheCreationInputTokens: input?.cache_creation, + reasoningOutputTokens: output?.reasoning, + textInputTokens: input?.text, + textOutputTokens: output?.text, + imageInputTokens: input?.image, + imageOutputTokens: output?.image, + audioInputTokens: input?.audio, + audioOutputTokens: output?.audio, + }; +} + +// Older integrations report usage only in `llmOutput.tokenUsage`. +function legacyUsage(usage: Metadata | undefined): TokenUsage | undefined { + if (!usage) return undefined; + + return { + inputTokens: number(usage["promptTokens"]), + outputTokens: number(usage["completionTokens"]), + totalTokens: number(usage["totalTokens"]), + }; +} + +// LangChain gives a message without a provider id the id of its run, as "run-". +function providerId(id: string | undefined): string | undefined { + return id?.startsWith("run-") ? undefined : id; +} + +function record(value: unknown): Metadata | undefined { + return value !== null && typeof value === "object" ? (value as Metadata) : undefined; +} + +function string(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +function number(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function strings(value: unknown): string[] | undefined { + return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string") + ? value + : undefined; +} diff --git a/packages/langchain/src/handler.ts b/packages/langchain/src/handler.ts new file mode 100644 index 0000000..6dbfcd2 --- /dev/null +++ b/packages/langchain/src/handler.ts @@ -0,0 +1,256 @@ +import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; +import type { DocumentInterface } from "@langchain/core/documents"; +import type { Serialized } from "@langchain/core/load/serializable"; +import { type BaseMessage, isBaseMessage } from "@langchain/core/messages"; +import type { LLMResult } from "@langchain/core/outputs"; +import type { ChainValues } from "@langchain/core/utils/types"; +import { + type SpanFields, + type SpanHandle, + startSpan, + type StartSpanOptions, +} from "@telemetry-dev/sdk"; + +import { completionFields, generationRequest, toolInput, toolOutput } from "./generation.ts"; +import { identityAttributes, userMetadata } from "./metadata.ts"; +import { plain, plainDocument, plainMessage } from "./plain.ts"; + +type Metadata = Record; + +// LangChain marks its own plumbing runs (LangGraph's __start__ and channel writes) with this tag. +const HIDDEN_TAG = "langsmith:hidden"; + +/** + * Records LangChain and LangGraph runs as telemetry.dev spans: chains and graph nodes, chat and + * completion model calls, tools, and retrievers. Pass it in `callbacks` of a run config; LangChain + * hands it down to every child run. Spans export through the client of `init()` from + * `@telemetry-dev/sdk`, and without one the handler records nothing. + */ +export class TelemetryDevCallbackHandler extends BaseCallbackHandler { + name = "telemetry_dev"; + + // Visible runs by run id, and for each hidden run the id of its nearest visible ancestor. + private readonly spans = new Map(); + private readonly hidden = new Map(); + private readonly firstChunks = new Map(); + private readonly graphs = new Set(); + + constructor() { + // LangChain runs a handler that it does not await in a shared background queue. There the + // active context is no longer the caller's, and a span would not nest in the caller's span. + super({ _awaitHandler: true }); + } + + // The declared parameter order of handleChainStart differs from the order LangChain calls it + // with: (chain, inputs, runId, parentRunId, tags, metadata, runType, runName). + override handleChainStart( + chain: Serialized, + inputs: ChainValues, + runId: string, + parentRunId?: string, + tags?: string[], + metadata?: Metadata, + _runType?: string, + runName?: string, + ): void { + if (tags?.includes(HIDDEN_TAG)) { + this.hidden.set(runId, this.visible(parentRunId)); + + return; + } + + const name = runName ?? lastId(chain) ?? "chain"; + const isGraph = chain?.id?.[0] === "langgraph"; + + if (isGraph) this.graphs.add(runId); + + this.start(runId, parentRunId, isGraph ? `invoke_agent ${name}` : name, metadata, { + type: isGraph ? "agent" : "span", + agentName: isGraph ? name : undefined, + input: plain(inputs), + }); + } + + override handleChainEnd(outputs: ChainValues, runId: string): void { + this.end(runId, { output: this.graphs.has(runId) ? agentOutput(outputs) : plain(outputs) }); + } + + override handleChainError(error: unknown, runId: string): void { + this.end(runId, { error }); + } + + override handleChatModelStart( + llm: Serialized, + messages: BaseMessage[][], + runId: string, + parentRunId?: string, + extraParams?: Metadata, + _tags?: string[], + metadata?: Metadata, + ): void { + const request = generationRequest(extraParams, metadata); + + this.startGeneration(runId, parentRunId, metadata, { + ...request, + name: `chat ${request.model ?? lastId(llm) ?? "unknown"}`, + input: (messages[0] ?? []).map(plainMessage), + }); + } + + override handleLLMStart( + llm: Serialized, + prompts: string[], + runId: string, + parentRunId?: string, + extraParams?: Metadata, + _tags?: string[], + metadata?: Metadata, + ): void { + const request = generationRequest(extraParams, metadata); + + this.startGeneration(runId, parentRunId, metadata, { + ...request, + name: `text_completion ${request.model ?? lastId(llm) ?? "unknown"}`, + input: prompts[0], + attributes: { ...request.attributes, "gen_ai.operation.name": "text_completion" }, + }); + } + + override handleLLMNewToken(token: string, _idx: unknown, runId: string): void { + const span = this.spans.get(runId); + const chunks = this.firstChunks.get(runId); + + if (!span || !chunks || token === "") return; + + span.recordOutputChunk(); + + if (chunks.seen) return; + chunks.seen = true; + span.update({ timeToFirstChunkMs: performance.now() - chunks.startedAt }); + } + + override handleLLMEnd(output: LLMResult, runId: string): void { + this.end(runId, completionFields(output)); + } + + override handleLLMError(error: unknown, runId: string): void { + this.end(runId, { error }); + } + + override handleToolStart( + tool: Serialized, + input: string, + runId: string, + parentRunId?: string, + _tags?: string[], + metadata?: Metadata, + runName?: string, + toolCallId?: string, + ): void { + const name = runName ?? lastId(tool) ?? "tool"; + + this.start(runId, parentRunId, `execute_tool ${name}`, metadata, { + type: "tool", + toolName: name, + toolCallId, + input: toolInput(input), + }); + } + + override handleToolEnd(output: unknown, runId: string): void { + this.end(runId, { output: toolOutput(output) }); + } + + override handleToolError(error: unknown, runId: string): void { + this.end(runId, { error }); + } + + override handleRetrieverStart( + retriever: Serialized, + query: string, + runId: string, + parentRunId?: string, + _tags?: string[], + metadata?: Metadata, + name?: string, + ): void { + this.start(runId, parentRunId, name ?? lastId(retriever) ?? "retriever", metadata, { + input: query, + }); + } + + override handleRetrieverEnd(documents: DocumentInterface[], runId: string): void { + this.end(runId, { output: documents.map(plainDocument) }); + } + + override handleRetrieverError(error: unknown, runId: string): void { + this.end(runId, { error }); + } + + private startGeneration( + runId: string, + parentRunId: string | undefined, + metadata: Metadata | undefined, + fields: SpanFields & { name: string }, + ): void { + const { name, ...rest } = fields; + this.start(runId, parentRunId, name, metadata, { ...rest, type: "generation" }); + this.firstChunks.set(runId, { startedAt: performance.now(), seen: false }); + } + + private start( + runId: string, + parentRunId: string | undefined, + name: string, + metadata: Metadata | undefined, + options: StartSpanOptions, + ): void { + const parentId = this.visible(parentRunId); + const parent = parentId === undefined ? undefined : this.spans.get(parentId); + + // LangChain hands metadata down to each child run, so the user's keys go on the top span only. + const span = startSpan(name, { + ...options, + parent, + metadata: parent ? undefined : userMetadata(metadata), + // telemetry.dev marks a span with a `langchain.*` attribute as a LangChain span. + attributes: { + ...identityAttributes(metadata), + ...options.attributes, + "langchain.run.id": runId, + }, + }); + + this.spans.set(runId, span); + } + + private end(runId: string, fields: SpanFields): void { + this.hidden.delete(runId); + this.graphs.delete(runId); + this.firstChunks.delete(runId); + const span = this.spans.get(runId); + + if (!span) return; + this.spans.delete(runId); + span.end(fields); + } + + private visible(runId: string | undefined): string | undefined { + return runId !== undefined && this.hidden.has(runId) ? this.hidden.get(runId) : runId; + } +} + +/** + * The answer of a graph whose state holds messages: its last message. The whole state holds each + * step of the run again, and the model, tool, and node spans already record the steps. + */ +function agentOutput(outputs: ChainValues): unknown { + const messages: unknown = outputs?.["messages"]; + const last: unknown = Array.isArray(messages) ? messages.at(-1) : undefined; + + return isBaseMessage(last) ? [plainMessage(last)] : plain(outputs); +} + +function lastId(serialized: Serialized | undefined): string | undefined { + return serialized?.id?.at(-1); +} diff --git a/packages/langchain/src/index.ts b/packages/langchain/src/index.ts new file mode 100644 index 0000000..c8a35bc --- /dev/null +++ b/packages/langchain/src/index.ts @@ -0,0 +1 @@ +export { TelemetryDevCallbackHandler } from "./handler.ts"; diff --git a/packages/langchain/src/metadata.ts b/packages/langchain/src/metadata.ts new file mode 100644 index 0000000..234e407 --- /dev/null +++ b/packages/langchain/src/metadata.ts @@ -0,0 +1,50 @@ +type Metadata = Record; + +// LangGraph copies `configurable.thread_id` into the metadata of each run of the graph. +const SESSION_KEYS = ["sessionId", "session_id", "thread_id", "conversation_id"]; +const USER_KEYS = ["userId", "user_id"]; +const IDENTITY_KEYS = new Set([...SESSION_KEYS, ...USER_KEYS]); + +// Keys that LangChain and LangGraph add to the metadata of a run. +const INTERNAL_PREFIXES = ["ls_", "langgraph_", "checkpoint_", "__"]; +const INTERNAL_KEYS = new Set(["versions"]); + +/** The session and user of a run, from the keys of its metadata. */ +export function identityAttributes(metadata: Metadata | undefined): Record { + const attributes: Record = {}; + const session = firstId(metadata, SESSION_KEYS); + const user = firstId(metadata, USER_KEYS); + + if (session !== undefined) attributes["gen_ai.conversation.id"] = session; + + if (user !== undefined) attributes["user.id"] = user; + + return attributes; +} + +/** The metadata that the app gave the run, without the keys of LangChain and LangGraph. */ +export function userMetadata(metadata: Metadata | undefined): Metadata | undefined { + if (!metadata) return undefined; + + const entries = Object.entries(metadata).filter( + ([key, value]) => + value !== undefined && + !IDENTITY_KEYS.has(key) && + !INTERNAL_KEYS.has(key) && + !INTERNAL_PREFIXES.some((prefix) => key.startsWith(prefix)), + ); + + return entries.length > 0 ? Object.fromEntries(entries) : undefined; +} + +function firstId(metadata: Metadata | undefined, keys: string[]): string | undefined { + for (const key of keys) { + const value = metadata?.[key]; + + if (typeof value === "string" && value.length > 0) return value; + + if (typeof value === "number" || typeof value === "bigint") return value.toString(); + } + + return undefined; +} diff --git a/packages/langchain/src/plain.ts b/packages/langchain/src/plain.ts new file mode 100644 index 0000000..596cd79 --- /dev/null +++ b/packages/langchain/src/plain.ts @@ -0,0 +1,89 @@ +import type { DocumentInterface } from "@langchain/core/documents"; +import { + type BaseMessage, + isAIMessage, + isBaseMessage, + isToolMessage, +} from "@langchain/core/messages"; + +// Past this depth, values go to JSON.stringify as they are; this also stops at reference cycles. +const MAX_DEPTH = 8; + +const ROLES: Record = { + human: "user", + ai: "assistant", + system: "system", + developer: "developer", + tool: "tool", + function: "function", +}; + +// Anthropic, LangChain standard, OpenAI Responses, and Gemini spellings. +const TOOL_CALL_BLOCKS = new Set(["tool_use", "tool_call", "function_call", "functionCall"]); + +/** + * A copy of a run's input or output in which messages, prompt values, and documents are plain + * objects. Without it, these LangChain objects serialize to their verbose constructor form. + */ +export function plain(value: unknown, depth = 0): unknown { + if (value === null || typeof value !== "object" || depth > MAX_DEPTH) return value; + + if (isBaseMessage(value)) return plainMessage(value); + + if (isPromptValue(value)) return value.toChatMessages().map(plainMessage); + + if (isDocument(value)) return plainDocument(value); + + if (Array.isArray(value)) return value.map((item: unknown) => plain(item, depth + 1)); + + const prototype: unknown = Object.getPrototypeOf(value); + + if (prototype !== Object.prototype && prototype !== null) return value; + + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, plain(item, depth + 1)]), + ); +} + +/** A message in the OpenAI chat shape that the ingest reads. */ +export function plainMessage(message: BaseMessage): Record { + const role = + message.type === "generic" + ? String((message as BaseMessage & { role?: unknown }).role) + : (ROLES[message.type] ?? message.type); + + const result: Record = { role, content: message.content }; + + if (message.name) result.name = message.name; + + if (isAIMessage(message) && message.tool_calls?.length) { + // The provider's own tool call blocks repeat the tool calls in the content. + if (Array.isArray(message.content)) { + result.content = message.content.filter( + (block) => !TOOL_CALL_BLOCKS.has(String((block as { type?: unknown }).type)), + ); + } + + result.tool_calls = message.tool_calls.map((call) => ({ + id: call.id, + type: "function", + function: { name: call.name, arguments: JSON.stringify(call.args) }, + })); + } + + if (isToolMessage(message)) result.tool_call_id = message.tool_call_id; + + return result; +} + +export function plainDocument(document: DocumentInterface): Record { + return { pageContent: document.pageContent, metadata: document.metadata }; +} + +function isPromptValue(value: object): value is { toChatMessages(): BaseMessage[] } { + return "toChatMessages" in value && typeof value.toChatMessages === "function"; +} + +function isDocument(value: object): value is DocumentInterface { + return "pageContent" in value && typeof value.pageContent === "string" && "metadata" in value; +} diff --git a/packages/langchain/tests/handler.test.ts b/packages/langchain/tests/handler.test.ts new file mode 100644 index 0000000..15c8a6c --- /dev/null +++ b/packages/langchain/tests/handler.test.ts @@ -0,0 +1,394 @@ +import { SpanStatusCode } from "@opentelemetry/api"; +import { InMemorySpanExporter, type ReadableSpan } from "@opentelemetry/sdk-trace-base"; +import type { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager"; +import { BaseChatModel } from "@langchain/core/language_models/chat_models"; +import { + AIMessage, + AIMessageChunk, + type BaseMessage, + HumanMessage, +} from "@langchain/core/messages"; +import { ChatGenerationChunk, type ChatResult } from "@langchain/core/outputs"; +import { ChatPromptTemplate } from "@langchain/core/prompts"; +import { RunnableLambda } from "@langchain/core/runnables"; +import { tool } from "@langchain/core/tools"; +import { Annotation, END, START, StateGraph } from "@langchain/langgraph"; +import { flush, init, shutdown, startActiveSpan } from "@telemetry-dev/sdk"; +import { afterEach, expect, test } from "vitest"; +import { z } from "zod"; + +import { TelemetryDevCallbackHandler } from "../src/index.ts"; + +class TestChatModel extends BaseChatModel { + constructor( + private readonly reply: AIMessage, + private readonly tools: unknown[] = [], + ) { + super({}); + } + + override invocationParams() { + return { tools: this.tools }; + } + + _llmType(): string { + return "test"; + } + + override getLsParams() { + return { + ls_provider: "google_genai", + ls_model_name: "gemini-test", + ls_model_type: "chat" as const, + ls_temperature: 0.2, + ls_max_tokens: 64, + ls_stop: ["END"], + }; + } + + async _generate(): Promise { + return { generations: [{ message: this.reply, text: this.reply.text }] }; + } + + override async *_streamResponseChunks( + _messages: BaseMessage[], + _options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun, + ): AsyncGenerator { + for (const text of ["Hel", "lo"]) { + // As @langchain/anthropic streams, the last chunk carries the model and stop reason. + const additional_kwargs = + text === "lo" ? { model: "claude-streamed", stop_reason: "end_turn" } : {}; + const message = new AIMessageChunk({ content: text, additional_kwargs }); + const chunk = new ChatGenerationChunk({ message, text }); + yield chunk; + await runManager?.handleLLMNewToken(text, undefined, undefined, undefined, undefined, { + chunk, + }); + } + } +} + +const reply = new AIMessage({ + content: "Hello", + id: "msg_123", + response_metadata: { model_name: "gemini-test-001", finish_reason: "stop" }, + usage_metadata: { + input_tokens: 12, + output_tokens: 3, + total_tokens: 15, + input_token_details: { cache_read: 4 }, + output_token_details: { reasoning: 1 }, + }, +}); + +function setup(): InMemorySpanExporter { + const spans = new InMemorySpanExporter(); + init( + { + apiKey: "td_live_test", + serviceName: "langchain-tests", + environment: "test", + exportMode: "immediate", + logLevel: "silent", + fetch: async () => new Response(null, { status: 200 }), + }, + { spanExporter: spans }, + ); + + return spans; +} + +async function finished(spans: InMemorySpanExporter): Promise { + await flush(); + + return spans.getFinishedSpans(); +} + +function byName(spans: ReadableSpan[], name: string): ReadableSpan { + const span = spans.find((s) => s.name === name); + expect(span, `span ${name}`).toBeDefined(); + + return span!; +} + +function parentOf(span: ReadableSpan): string | undefined { + return span.parentSpanContext?.spanId; +} + +afterEach(async () => { + await shutdown(); +}); + +test("records a chain with its prompt and model call as one span tree", async () => { + const spans = setup(); + const prompt = ChatPromptTemplate.fromMessages([ + ["system", "Be brief."], + ["human", "{question}"], + ]); + + await prompt.pipe(new TestChatModel(reply)).invoke( + { question: "Hi?" }, + { + callbacks: [new TelemetryDevCallbackHandler()], + metadata: { userId: "user-1", session_id: "session-1", plan: "pro" }, + }, + ); + + const all = await finished(spans); + const root = byName(all, "RunnableSequence"); + const promptSpan = byName(all, "ChatPromptTemplate"); + const chat = byName(all, "chat gemini-test"); + + expect(all).toHaveLength(3); + expect(parentOf(promptSpan)).toBe(root.spanContext().spanId); + expect(parentOf(chat)).toBe(root.spanContext().spanId); + expect(new Set(all.map((s) => s.spanContext().traceId)).size).toBe(1); + expect(new Set(all.map((s) => s.attributes["langchain.run.id"])).size).toBe(3); + + expect(root.attributes).toMatchObject({ + "gen_ai.operation.name": "function", + "gen_ai.input.messages": '{"question":"Hi?"}', + "gen_ai.conversation.id": "session-1", + "user.id": "user-1", + "td.metadata.plan": "pro", + }); + expect(root.attributes["gen_ai.output.messages"]).toBe('{"role":"assistant","content":"Hello"}'); + expect(chat.attributes["td.metadata.plan"]).toBeUndefined(); + + expect(chat.attributes).toMatchObject({ + "gen_ai.operation.name": "chat", + "gen_ai.request.model": "gemini-test", + "gen_ai.provider.name": "gcp.gen_ai", + "gen_ai.request.temperature": 0.2, + "gen_ai.request.max_tokens": 64, + "gen_ai.request.stop_sequences": ["END"], + "gen_ai.response.model": "gemini-test-001", + "gen_ai.response.id": "msg_123", + "gen_ai.response.finish_reasons": ["stop"], + "gen_ai.usage.input_tokens": 12, + "gen_ai.usage.output_tokens": 3, + "gen_ai.usage.total_tokens": 15, + "gen_ai.usage.cache_read.input_tokens": 4, + "gen_ai.usage.reasoning.output_tokens": 1, + "gen_ai.conversation.id": "session-1", + "user.id": "user-1", + "gen_ai.input.messages": + '[{"role":"system","content":"Be brief."},{"role":"user","content":"Hi?"}]', + "gen_ai.output.messages": '[{"role":"assistant","content":"Hello"}]', + }); +}); + +test("records a LangGraph run as an agent span with node spans and skips hidden runs", async () => { + const spans = setup(); + const State = Annotation.Root({ question: Annotation, answer: Annotation }); + const model = new TestChatModel(reply); + + const graph = new StateGraph(State) + .addNode("respond", async (state) => ({ + answer: (await model.invoke(state.question)).text, + })) + .addEdge(START, "respond") + .addEdge("respond", END) + .compile(); + + await graph.invoke( + { question: "Hi?" }, + { callbacks: [new TelemetryDevCallbackHandler()], configurable: { thread_id: "thread-1" } }, + ); + + const all = await finished(spans); + const agent = byName(all, "invoke_agent LangGraph"); + const node = byName(all, "respond"); + const chat = byName(all, "chat gemini-test"); + + expect(all.map((s) => s.name)).not.toContain("__start__"); + expect(agent.attributes).toMatchObject({ + "gen_ai.operation.name": "invoke_agent", + "gen_ai.agent.name": "LangGraph", + }); + expect(parentOf(node)).toBe(agent.spanContext().spanId); + expect(parentOf(chat)).toBe(node.spanContext().spanId); + + for (const span of all) { + expect(span.attributes["gen_ai.conversation.id"]).toBe("thread-1"); + expect(span.attributes["td.metadata.thread_id"]).toBeUndefined(); + } +}); + +test("records the answer of a graph with a message state as its output", async () => { + const spans = setup(); + const State = Annotation.Root({ + messages: Annotation({ reducer: (a, b) => a.concat(b), default: () => [] }), + }); + const model = new TestChatModel(reply); + + const graph = new StateGraph(State) + .addNode("respond", async (state) => ({ messages: [await model.invoke(state.messages)] })) + .addEdge(START, "respond") + .addEdge("respond", END) + .compile(); + + await graph.invoke( + { messages: [new HumanMessage("Hi?")] }, + { callbacks: [new TelemetryDevCallbackHandler()] }, + ); + + const agent = byName(await finished(spans), "invoke_agent LangGraph"); + + expect(agent.attributes["gen_ai.input.messages"]).toBe( + '{"messages":[{"role":"user","content":"Hi?"}]}', + ); + expect(agent.attributes["gen_ai.output.messages"]).toBe( + '[{"role":"assistant","content":"Hello"}]', + ); +}); + +test("records the offered tools and each tool call of a reply once", async () => { + const spans = setup(); + + const toolReply = new AIMessage({ + content: [ + { type: "text", text: "Adding." }, + { type: "tool_use", id: "call_1", name: "add", input: { a: 1, b: 2 } }, + ], + tool_calls: [{ id: "call_1", name: "add", args: { a: 1, b: 2 }, type: "tool_call" }], + }); + + const tools = [ + { type: "function", function: { name: "add", description: "Add.", parameters: {} } }, + { name: "multiply", description: "Multiply.", input_schema: {} }, + { type: "web_search_20250305", name: "web_search" }, + { functionDeclarations: [{ name: "divide" }, { name: "subtract" }] }, + ]; + + await new TestChatModel(toolReply, tools).invoke("Add 1 and 2.", { + callbacks: [new TelemetryDevCallbackHandler()], + }); + + const chat = byName(await finished(spans), "chat gemini-test"); + + expect(JSON.parse(String(chat.attributes["gen_ai.tool.definitions"]))).toEqual([ + { type: "function", name: "add" }, + { type: "function", name: "multiply" }, + { type: "web_search_20250305", name: "web_search" }, + { type: "function", name: "divide" }, + { type: "function", name: "subtract" }, + ]); + expect(JSON.parse(String(chat.attributes["gen_ai.output.messages"]))).toEqual([ + { + role: "assistant", + content: [{ type: "text", text: "Adding." }], + tool_calls: [ + { id: "call_1", type: "function", function: { name: "add", arguments: '{"a":1,"b":2}' } }, + ], + }, + ]); +}); + +test("records a tool call with its arguments, call id, and result", async () => { + const spans = setup(); + + const add = tool(async ({ a, b }: { a: number; b: number }) => String(a + b), { + name: "add", + description: "Add two numbers.", + schema: z.object({ a: z.number(), b: z.number() }), + }); + + await add.invoke( + { id: "call_1", name: "add", args: { a: 1, b: 2 }, type: "tool_call" }, + { callbacks: [new TelemetryDevCallbackHandler()] }, + ); + + const span = byName(await finished(spans), "execute_tool add"); + + expect(span.attributes).toMatchObject({ + "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.name": "add", + "gen_ai.tool.call.id": "call_1", + "gen_ai.tool.call.arguments": '{"a":1,"b":2}', + "gen_ai.tool.call.result": "3", + }); +}); + +test("marks the failed run and each run that it fails", async () => { + const spans = setup(); + + const fail = RunnableLambda.from((): string => { + throw new TypeError("bad input"); + }).withConfig({ runName: "fail" }); + + const chain = RunnableLambda.from((input: string) => input) + .pipe(fail) + .withConfig({ runName: "outer" }); + + await expect( + chain.invoke("x", { callbacks: [new TelemetryDevCallbackHandler()] }), + ).rejects.toThrow("bad input"); + + const all = await finished(spans); + + for (const name of ["outer", "fail"]) { + const span = byName(all, name); + expect(span.status.code).toBe(SpanStatusCode.ERROR); + expect(span.attributes["error.type"]).toBe("TypeError"); + } +}); + +test("nests the top run in the active span of the app", async () => { + const spans = setup(); + + await startActiveSpan("handle-request", () => + new TestChatModel(reply).invoke("Hi?", { callbacks: [new TelemetryDevCallbackHandler()] }), + ); + + const all = await finished(spans); + + expect(parentOf(byName(all, "chat gemini-test"))).toBe( + byName(all, "handle-request").spanContext().spanId, + ); +}); + +test("nests concurrent runs each in its own app span", async () => { + const spans = setup(); + const handler = new TelemetryDevCallbackHandler(); + const slow = RunnableLambda.from(async (input: string) => { + await new Promise((resolve) => setTimeout(resolve, 5)); + + return input; + }); + + await Promise.all( + ["first", "second", "third"].map((name) => + startActiveSpan(name, () => + slow.withConfig({ runName: `run ${name}` }).invoke(name, { callbacks: [handler] }), + ), + ), + ); + + const all = await finished(spans); + + for (const name of ["first", "second", "third"]) { + expect(parentOf(byName(all, `run ${name}`))).toBe(byName(all, name).spanContext().spanId); + } +}); + +test("records the time to the first chunk and the response of a streamed call", async () => { + const spans = setup(); + const stream = await new TestChatModel(reply).stream("Hi?", { + callbacks: [new TelemetryDevCallbackHandler()], + }); + + for await (const _chunk of stream) { + // Read the stream to its end. + } + + const chat = byName(await finished(spans), "chat gemini-test"); + + expect(chat.attributes["gen_ai.response.time_to_first_chunk"]).toBeGreaterThanOrEqual(0); + expect(chat.attributes["gen_ai.response.model"]).toBe("claude-streamed"); + expect(chat.attributes["gen_ai.response.finish_reasons"]).toEqual(["end_turn"]); + expect(chat.attributes["gen_ai.output.messages"]).toBe( + '[{"role":"assistant","content":"Hello"}]', + ); +}); diff --git a/packages/langchain/tsconfig.json b/packages/langchain/tsconfig.json new file mode 100644 index 0000000..ff4adab --- /dev/null +++ b/packages/langchain/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "nodenext", + "moduleResolution": "nodenext", + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + } +} diff --git a/packages/langchain/vite.config.ts b/packages/langchain/vite.config.ts new file mode 100644 index 0000000..949ff13 --- /dev/null +++ b/packages/langchain/vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from "vite-plus"; +import { customExports } from "../../scripts/package-exports.mjs"; + +export default defineConfig({ + resolve: { + alias: [ + { + find: "@telemetry-dev/sdk", + replacement: new URL("../sdk/src/index.ts", import.meta.url).pathname, + }, + ], + }, + pack: { + entry: ["src/index.ts"], + dts: true, + exports: { + devExports: "bun", + customExports, + }, + }, + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + }, + fmt: {}, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98ae28f..8ab341d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,12 @@ catalogs: '@google/genai': specifier: 2.23.0 version: 2.23.0 + '@langchain/core': + specifier: 1.3.4 + version: 1.3.4 + '@langchain/langgraph': + specifier: 1.7.0 + version: 1.7.0 '@opentelemetry/api': specifier: ^1.9.1 version: 1.9.1 @@ -301,7 +307,7 @@ importers: devDependencies: '@google/genai': specifier: 'catalog:' - version: 2.23.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) + version: 2.23.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3)) '@opentelemetry/sdk-logs': specifier: 'catalog:' version: 0.223.0(@opentelemetry/api@1.9.1) @@ -327,6 +333,39 @@ importers: specifier: npm:@voidzero-dev/vite-plus-test@0.1.20 version: '@voidzero-dev/vite-plus-test@0.1.20(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(esbuild@0.27.7)(jiti@2.7.0)(jsdom@28.1.0(@noble/hashes@2.2.0))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + packages/langchain: + devDependencies: + '@langchain/core': + specifier: 'catalog:' + version: 1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3) + '@langchain/langgraph': + specifier: 'catalog:' + version: 1.7.0(@langchain/core@1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + '@opentelemetry/api': + specifier: 'catalog:' + version: 1.9.1 + '@opentelemetry/sdk-trace-base': + specifier: 'catalog:' + version: 2.12.0(@opentelemetry/api@1.9.1) + '@telemetry-dev/sdk': + specifier: workspace:* + version: link:../sdk + '@types/node': + specifier: 'catalog:' + version: 24.12.4 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite-plus: + specifier: 'catalog:' + version: 0.1.20(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@24.12.4)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(esbuild@0.27.7)(jiti@2.7.0)(jsdom@28.1.0(@noble/hashes@2.2.0))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + vitest: + specifier: npm:@voidzero-dev/vite-plus-test@0.1.20 + version: '@voidzero-dev/vite-plus-test@0.1.20(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@24.12.4)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(esbuild@0.27.7)(jiti@2.7.0)(jsdom@28.1.0(@noble/hashes@2.2.0))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + zod: + specifier: 'catalog:' + version: 4.4.3 + packages/mcp: devDependencies: '@modelcontextprotocol/client': @@ -549,7 +588,7 @@ importers: devDependencies: '@earendil-works/pi-coding-agent': specifier: 0.82.1 - version: 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + version: 0.82.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) '@opentelemetry/sdk-logs': specifier: 'catalog:' version: 0.223.0(@opentelemetry/api@1.9.1) @@ -884,6 +923,9 @@ packages: '@bufbuild/protobuf@2.13.0': resolution: {integrity: sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==} + '@cfworker/json-schema@4.1.6': + resolution: {integrity: sha512-YzVGTHXf4bzlyAL5KAvHYCdNbbt6zM5cYkAgxqxQPDQM6aZ4ShIIN5yJ+6q7YeonnevxT8k/LmD2tJi/jOiKkg==} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -1328,6 +1370,38 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@langchain/core@1.3.4': + resolution: {integrity: sha512-4LnkVpxOzA4BuMPuAQKDDf4SujSzd4WahKk51eyUJzSEsQjyGpbyAgDRWPYr3wzgtAJ59UXewemsyIxRKTXFrA==} + engines: {node: '>=20'} + + '@langchain/langgraph-checkpoint@1.2.2': + resolution: {integrity: sha512-KL3nzi7I61ll5nANCQe5RoJ5SqTFrE+si9IK6k7IIj9OrPkIKT/Dz0w8CyJF+K7OEuJfA33HvPrZqfzzW1DXDA==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.1.48 + + '@langchain/langgraph-sdk@1.13.0': + resolution: {integrity: sha512-/DakXLkn5i9kCxmPzHeqg0lhBcqnjrt2QuySNfSZ25tLQWfEj2ljh66iUvXMhXMasSmLbNULAGgQ/7jWciwxKA==} + peerDependencies: + '@langchain/core': ^1.3.2 + react: ^18 || ^19 + react-dom: ^18 || ^19 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@langchain/langgraph@1.7.0': + resolution: {integrity: sha512-JERqjrDrYzJc/dSz65uBnAJE0vmRmgCWakBsYGkOiYyA/NTkxCO4k7DXq/ehFUlQmqo+fa/iHOkXKObuaqT7ew==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.3.4 + zod: ^3.25.32 || ^4.2.0 + + '@langchain/protocol@0.0.20': + resolution: {integrity: sha512-sxlkDG2/HGyjDFLTJ8B/o3oglRjOv8iLCFgrV0550/Vym3V6RZ0SUjAQFe8jrLQw8ZXjERy3swJp5OODBSrUYA==} + '@mariozechner/clipboard-darwin-arm64@0.3.9': resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} engines: {node: '>= 10'} @@ -2199,10 +2273,16 @@ packages: '@smithy/core@3.29.1': resolution: {integrity: sha512-qoiY4nrk5OCu1+eIR1VB8l5DmON/oKiqrd5zZFAhXJXjJlLWQusKEW/SkBDAtGDcPaz86m9kfcE1lngU0GlM6A==} engines: {node: '>=18.0.0'} + deprecated: |- + Deprecated due to a bug in custom logger data redaction. Upgrade to the latest version. If you used a custom logger with this + version, review your logs and rotate any credentials or other secrets. See https://github.com/aws/aws-sdk-js-v3/issues/8393. '@smithy/core@3.34.1': resolution: {integrity: sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw==} engines: {node: '>=18.0.0'} + deprecated: |- + Deprecated due to a bug in custom logger data redaction. Upgrade to the latest version. If you used a custom logger with this + version, review your logs and rotate any credentials or other secrets. See https://github.com/aws/aws-sdk-js-v3/issues/8393. '@smithy/credential-provider-imds@4.5.2': resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} @@ -2293,6 +2373,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@24.12.4': resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} @@ -3067,6 +3150,12 @@ packages: microsandbox: optional: true + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -3322,6 +3411,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-network-error@1.3.2: + resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==} + engines: {node: '>=16'} + is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} @@ -3352,6 +3445,9 @@ packages: jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + jsdom@28.1.0: resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -3399,6 +3495,26 @@ packages: resolution: {integrity: sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==} engines: {node: '>=20.0.0'} + langsmith@0.11.8: + resolution: {integrity: sha512-rAskvQCNjwGqwjA9fOJGugZ1vLB66yaivxWGSTA9ldtF9+P00gvXH1DEr0kW798cyUyBlZolFO0kPb//zDNc7w==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + ws: '>=7' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + ws: + optional: true + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -3598,6 +3714,10 @@ packages: mupdf@1.28.0: resolution: {integrity: sha512-ACUnbpECaQ5JLq04pwd89lS+0IGMest5qL5tb08g9TAR7bDtfqflHEkb2Xm3o4rvC/szguLiV+WEbW9kstj8Sg==} + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3797,10 +3917,34 @@ packages: oxlint-tsgolint: optional: true + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-queue@9.3.5: + resolution: {integrity: sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==} + engines: {node: '>=20'} + p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} + p-retry@7.1.1: + resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} + engines: {node: '>=20'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-timeout@7.0.2: + resolution: {integrity: sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==} + engines: {node: '>=20'} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -4479,18 +4623,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.21.3: resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} @@ -4931,6 +5063,8 @@ snapshots: '@bufbuild/protobuf@2.13.0': {} + '@cfworker/json-schema@4.1.6': {} + '@colors/colors@1.6.0': {} '@csstools/color-helpers@6.0.2': @@ -4969,9 +5103,9 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 - '@earendil-works/pi-agent-core@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.82.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) diff: 8.0.4 ignore: 7.0.5 typebox: 1.1.38 @@ -4984,11 +5118,11 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-ai@0.82.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3)) '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) '@opentelemetry/api': 1.9.0 '@smithy/node-http-handler': 4.7.3 @@ -5005,10 +5139,10 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-coding-agent@0.82.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) - '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-agent-core': 0.82.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) '@earendil-works/pi-tui': 0.82.1 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -5141,27 +5275,27 @@ snapshots: '@noble/hashes': 2.2.0 optional: true - '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))': dependencies: google-auth-library: 10.9.0 p-retry: 4.6.2 protobufjs: 7.6.5 - ws: 8.20.0 + ws: 8.21.3 optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@google/genai@2.23.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': + '@google/genai@2.23.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3))': dependencies: google-auth-library: 10.9.0 p-retry: 4.6.2 protobufjs: 7.6.5 ws: 8.21.3 optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3) transitivePeerDependencies: - bufferutil - supports-color @@ -5305,6 +5439,51 @@ snapshots: '@kurkle/color@0.3.4': {} + '@langchain/core@1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3)': + dependencies: + '@cfworker/json-schema': 4.1.6 + '@standard-schema/spec': 1.1.0 + js-tiktoken: 1.0.21 + langsmith: 0.11.8(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3) + mustache: 4.2.0 + p-queue: 6.6.2 + zod: 4.4.3 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - ws + + '@langchain/langgraph-checkpoint@1.2.2(@langchain/core@1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3))': + dependencies: + '@langchain/core': 1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3) + + '@langchain/langgraph-sdk@1.13.0(@langchain/core@1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@langchain/core': 1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3) + '@langchain/protocol': 0.0.20 + '@types/json-schema': 7.0.15 + p-queue: 9.3.5 + p-retry: 7.1.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@langchain/langgraph@1.7.0(@langchain/core@1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3)': + dependencies: + '@langchain/core': 1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3) + '@langchain/langgraph-checkpoint': 1.2.2(@langchain/core@1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3)) + '@langchain/langgraph-sdk': 1.13.0(@langchain/core@1.3.4(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@langchain/protocol': 0.0.20 + '@standard-schema/spec': 1.1.0 + zod: 4.4.3 + transitivePeerDependencies: + - react + - react-dom + + '@langchain/protocol@0.0.20': {} + '@mariozechner/clipboard-darwin-arm64@0.3.9': optional: true @@ -5352,7 +5531,7 @@ snapshots: '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/semantic-conventions': 1.40.0 - ws: 8.20.0 + ws: 8.21.3 zod: 4.4.3 zod-to-json-schema: 3.25.2(zod@4.4.3) optionalDependencies: @@ -5391,7 +5570,7 @@ snapshots: dependencies: zod: 4.4.3 - '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.6)(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.16) ajv: 8.20.0 @@ -5410,6 +5589,8 @@ snapshots: raw-body: 3.0.2 zod: 4.4.3 zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + '@cfworker/json-schema': 4.1.6 transitivePeerDependencies: - supports-color optional: true @@ -6151,6 +6332,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/json-schema@7.0.15': {} + '@types/node@24.12.4': dependencies: undici-types: 7.16.0 @@ -6283,7 +6466,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 vite: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@24.12.4)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' - ws: 8.20.0 + ws: 8.21.3 optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 24.12.4 @@ -6324,7 +6507,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 vite: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' - ws: 8.20.0 + ws: 8.21.3 optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 25.6.0 @@ -6365,7 +6548,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 vite: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.9.0)' - ws: 8.20.0 + ws: 8.21.3 optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 25.6.0 @@ -6909,6 +7092,10 @@ snapshots: - xml2js - zephyr-agent + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + eventsource-parser@3.0.8: {} eventsource@3.0.7: @@ -7237,6 +7424,8 @@ snapshots: ipaddr.js@1.9.1: optional: true + is-network-error@1.3.2: {} + is-obj@2.0.0: {} is-potential-custom-element-name@1.0.1: @@ -7257,6 +7446,10 @@ snapshots: jose@6.2.3: {} + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + jsdom@28.1.0(@noble/hashes@2.2.0): dependencies: '@acemir/cssom': 0.9.31 @@ -7330,6 +7523,16 @@ snapshots: kysely@0.28.17: optional: true + langsmith@0.11.8(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.9.1))(openai@7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3))(ws@8.21.3): + dependencies: + p-queue: 6.6.2 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/exporter-trace-otlp-proto': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.12.0(@opentelemetry/api@1.9.1) + openai: 7.18.0(@aws-sdk/credential-provider-node@3.972.83)(@smithy/signature-v4@5.7.3)(undici@8.9.0)(ws@8.21.3)(zod@4.4.3) + ws: 8.21.3 + lie@3.3.0: dependencies: immediate: 3.0.6 @@ -7502,6 +7705,8 @@ snapshots: mupdf@1.28.0: {} + mustache@4.2.0: {} + nanoid@3.3.12: {} negotiator@1.0.0: @@ -7725,11 +7930,33 @@ snapshots: '@oxlint/binding-win32-x64-msvc': 1.61.0 oxlint-tsgolint: 0.22.0 + p-finally@1.0.0: {} + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-queue@9.3.5: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.2 + p-retry@4.6.2: dependencies: '@types/retry': 0.12.0 retry: 0.13.1 + p-retry@7.1.1: + dependencies: + is-network-error: 1.3.2 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-timeout@7.0.2: {} + pako@1.0.11: {} parse5@8.0.1: @@ -7857,7 +8084,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.2 - '@types/node': 24.12.4 + '@types/node': 25.6.0 long: 5.3.2 proxy-addr@2.0.7: @@ -8545,8 +8772,6 @@ snapshots: wrappy@1.0.2: optional: true - ws@8.20.0: {} - ws@8.21.3: {} xml-name-validator@5.0.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0cbb24d..0c83751 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,6 +24,8 @@ catalog: "@aws-sdk/client-bedrock-agent-runtime": 3.1135.0 "@aws-sdk/client-bedrock-runtime": 3.1135.0 "@google/genai": 2.23.0 + "@langchain/core": 1.3.4 + "@langchain/langgraph": 1.7.0 "@types/node": ^24 ai: 7.0.111 cross-env: 10.1.0 diff --git a/release-please-config.json b/release-please-config.json index 79b72d6..5d4ca5b 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -50,6 +50,11 @@ "component": "google-genai", "release-type": "node" }, + "packages/langchain": { + "component": "langchain", + "release-type": "node", + "initial-version": "0.1.0" + }, "packages/mcp": { "component": "mcp", "release-type": "node"