diff --git a/apps/website/content/docs/licensing/api/api-docs.json b/apps/website/content/docs/licensing/api/api-docs.json index 97b130a17..060e22626 100644 --- a/apps/website/content/docs/licensing/api/api-docs.json +++ b/apps/website/content/docs/licensing/api/api-docs.json @@ -185,7 +185,7 @@ { "name": "emitNag", "kind": "function", - "description": "", + "description": "Emit one non-blocking license warning for a package/status pair.\n\n`licensed` and `noncommercial` statuses are silent. Other statuses warn once\nper package and status so repeated initialization does not spam logs.", "signature": "emitNag(result: Pick, options: EmitNagOptions): void", "params": [ { @@ -210,7 +210,7 @@ { "name": "evaluateLicense", "kind": "function", - "description": "", + "description": "Convert a verified token result into a license status.\n\n`verifyResult` may be undefined when no token was supplied. In that case a\ncaller-provided `isNoncommercial` hint returns `noncommercial`; otherwise the\nstatus is `missing`. Signature failures return `tampered`, and valid claims\nare evaluated against `exp` plus the optional grace window.", "signature": "evaluateLicense(verifyResult: VerifyResult | undefined, options: EvaluateOptions): EvaluateResult", "params": [ { @@ -247,7 +247,7 @@ { "name": "runLicenseCheck", "kind": "function", - "description": "", + "description": "Run the full package license check.\n\nThe helper verifies an optional token, evaluates the status, emits the\npackage nag warning when appropriate, and memoizes identical package/token\npairs so repeated provider initialization stays quiet.", "signature": "runLicenseCheck(options: RunLicenseCheckOptions): Promise", "params": [ { diff --git a/apps/website/content/docs/licensing/getting-started/introduction.mdx b/apps/website/content/docs/licensing/getting-started/introduction.mdx index 2b272cbbc..2196823a7 100644 --- a/apps/website/content/docs/licensing/getting-started/introduction.mdx +++ b/apps/website/content/docs/licensing/getting-started/introduction.mdx @@ -69,7 +69,7 @@ The higher-level check is built not to block app startup: - warning output goes through `console.warn` unless a custom `warn` function is supplied; - no network request is made by the licensing check. -The code returns statuses instead of throwing for normal license states. For me, that's the right default here: you get to decide what a missing or expired license means for your app instead of having the check make that call for you. The tradeoff is that nothing stops a consumer from ignoring the status entirely. `@threadplane/chat` treats it as a warning and visibility mechanism, not an app kill switch. +The code returns statuses instead of throwing for normal license states. That keeps policy decisions in the host package or application: a missing or expired license can be logged, surfaced, or enforced by the caller instead of being decided by initialization code. `@threadplane/chat` treats the status as a warning and visibility mechanism, not an app kill switch. ## Next steps diff --git a/apps/website/content/docs/middleware/api/client-tool-helpers.mdx b/apps/website/content/docs/middleware/api/client-tool-helpers.mdx index e1060948d..669747fdf 100644 --- a/apps/website/content/docs/middleware/api/client-tool-helpers.mdx +++ b/apps/website/content/docs/middleware/api/client-tool-helpers.mdx @@ -1,4 +1,4 @@ -# bindClientTools() +# LangGraph.js client-tool helpers `bindClientTools()` is the main helper from `@threadplane/middleware/langgraph`. It binds your server tools plus the current run's client-tool catalog onto a LangChain chat model. @@ -10,7 +10,7 @@ import { } from '@threadplane/middleware/langgraph'; ``` -There is no root JavaScript entry point for `@threadplane/middleware`; import from the `/langgraph` subpath. +There is no root JavaScript entry point for `@threadplane/middleware`; import from the `/langgraph` subpath. The Python package exposes equivalent snake_case helpers from `threadplane.middleware.langgraph`. Related helpers in the same entry point include `clientToolsChannel()`, `clientToolsRouter()`, `clientToolSpecs()`, `clientToolNames()`, `hasClientToolCall()`, `hasServerToolCall()`, `routeAfterAgent()`, and `lastMessage()`. diff --git a/apps/website/content/docs/middleware/getting-started/introduction.mdx b/apps/website/content/docs/middleware/getting-started/introduction.mdx index a5c0637f0..80b601060 100644 --- a/apps/website/content/docs/middleware/getting-started/introduction.mdx +++ b/apps/website/content/docs/middleware/getting-started/introduction.mdx @@ -1,8 +1,15 @@ # Introduction -`@threadplane/middleware` is the backend companion for Threadplane client tools. It lets a browser declare tools, lets the model call those tools, and routes client-tool-only turns back to the browser for execution. +Threadplane publishes middleware for backend graphs that need browser-executed client tools. The browser declares tools, the model can call those tools, and the backend routes client-tool-only turns back to the browser for execution. -The package currently publishes one runtime entry point: +There are two package surfaces: + +| Runtime | Package | Entry point | +|---------|---------|-------------| +| LangGraph.js | `@threadplane/middleware` | `@threadplane/middleware/langgraph` | +| Python LangGraph | `threadplane-middleware` | `threadplane.middleware.langgraph` | + +The TypeScript package currently publishes one runtime entry point: ```ts import { @@ -16,23 +23,23 @@ There is no root `@threadplane/middleware` JavaScript entry point. Import from ` ## What it does -The LangGraph entry point reads a client tool catalog from graph state, converts it into OpenAI function-tool objects, binds those tool stubs onto your chat model, and routes client-tool calls to `END` so the browser can execute them. +The LangGraph entry points read a client tool catalog from graph state, convert it into OpenAI function-tool objects, bind those tool stubs onto your chat model, and route client-tool calls to `END` so the browser can execute them. -The catalog is read from `state.tools` first. If that channel is absent or empty, it falls back to `state.client_tools`. +The catalog is read from `state.tools` first. If that channel is absent or empty, both packages fall back to `state.client_tools`. ## Runtime flow 1. The browser sends tool specs with the run request. -2. Your LangGraph node calls `bindClientTools()` inside the run, because the catalog can differ per request. +2. Your LangGraph node calls `bindClientTools()` or `bind_client_tools()` inside the run, because the catalog can differ per request. 3. The model emits a tool call for a browser-declared tool. -4. `clientToolsRouter()` routes client-only tool calls to `END`. +4. The router routes client-only tool calls to `END`. 5. The browser executes the local tool and resumes the graph with a `ToolMessage`. If a turn mixes server tool calls and client tool calls, server tools win the first route. The server tool node runs first, and the client call can surface on a later turn. -## Public surface +## TypeScript public surface -The entry point exports: +`@threadplane/middleware/langgraph` exports: | API | Purpose | |-----|---------| @@ -46,9 +53,23 @@ The entry point exports: | `routeAfterAgent()` | Lower-level routing helper used by `clientToolsRouter()`. | | `lastMessage()` | Reads the last message from state. | +## Python public surface + +`threadplane.middleware.langgraph` exports: + +| API | Purpose | +|-----|---------| +| `bind_client_tools()` | Binds server tools plus client-declared tool stubs onto a model. | +| `client_tool_specs()` | Converts state catalog entries into OpenAI function-tool specs. | +| `client_tool_names()` | Returns the set of client-declared tool names for a run. | +| `has_client_tool_call()` | Checks whether the last message calls a client tool. | +| `has_server_tool_call()` | Checks whether the last message calls a server or unknown tool. | +| `route_after_agent()` | Routing helper for conditional edges. | +| `last_message()` | Reads the last message from state. | + ## When to use it -Use middleware when you own a LangGraph.js backend and want browser-declared tools from `@threadplane/chat` to participate in model tool calling without executing browser-only code on the server. +Use middleware when you own a LangGraph backend and want browser-declared tools from `@threadplane/chat` to participate in model tool calling without executing browser-only code on the server. If your backend already speaks AG-UI, use `@threadplane/ag-ui` instead. If your frontend talks directly to LangGraph and does not need browser-executed tools, `@threadplane/langgraph` can run without this middleware. @@ -56,4 +77,5 @@ If your backend already speaks AG-UI, use `@threadplane/ag-ui` instead. If your - [Quick Start](/docs/middleware/getting-started/quickstart) - install and wire the LangGraph helper. - [LangGraph Client Tools](/docs/middleware/guides/langgraph-client-tools) - routing details and server-tool behavior. -- [Client Tool Helpers](/docs/middleware/api/client-tool-helpers) - generated API reference for the exported helpers. +- [Python LangGraph Middleware](/docs/middleware/guides/python-langgraph) - the Python package and snake_case helpers. +- [Client Tool Helpers](/docs/middleware/api/client-tool-helpers) - generated API reference for the TypeScript helpers. diff --git a/apps/website/content/docs/middleware/getting-started/quickstart.mdx b/apps/website/content/docs/middleware/getting-started/quickstart.mdx index ecd794d8f..08727ac9b 100644 --- a/apps/website/content/docs/middleware/getting-started/quickstart.mdx +++ b/apps/website/content/docs/middleware/getting-started/quickstart.mdx @@ -1,12 +1,12 @@ # Quick Start -Install the middleware package and its LangGraph peer dependencies: +Install the TypeScript middleware package and its LangGraph.js peer dependencies: ```bash npm install @threadplane/middleware @langchain/core @langchain/langgraph ``` -The package exposes its JavaScript API from `@threadplane/middleware/langgraph`. +The package exposes its JavaScript API from `@threadplane/middleware/langgraph`. For Python LangGraph, install `threadplane-middleware` and follow the [Python guide](/docs/middleware/guides/python-langgraph). ## Add client-tool state channels @@ -95,4 +95,5 @@ On the frontend, declare client tools with `@threadplane/chat` and send them thr ## Next steps - [LangGraph Client Tools](/docs/middleware/guides/langgraph-client-tools) - mixed server/client routing and helper behavior. +- [Python LangGraph Middleware](/docs/middleware/guides/python-langgraph) - the equivalent Python package and helper names. - [Client Tool Helpers](/docs/middleware/api/client-tool-helpers) - generated API details. diff --git a/apps/website/content/docs/middleware/guides/langgraph-client-tools.mdx b/apps/website/content/docs/middleware/guides/langgraph-client-tools.mdx index 8cc2df911..76a97604a 100644 --- a/apps/website/content/docs/middleware/guides/langgraph-client-tools.mdx +++ b/apps/website/content/docs/middleware/guides/langgraph-client-tools.mdx @@ -1,7 +1,9 @@ -# LangGraph Client Tools +# LangGraph.js Client Tools Client tools are frontend-declared tools that the model can call but the browser executes. The backend only exposes tool schemas to the model and decides whether a turn should continue on the server or end so the client can run a local tool. +This page documents the TypeScript `@threadplane/middleware/langgraph` entry point. The Python package uses the same routing model with snake_case helpers; see [Python LangGraph Middleware](/docs/middleware/guides/python-langgraph). + ## State shape The middleware reads this state slice: diff --git a/apps/website/content/docs/middleware/guides/python-langgraph.mdx b/apps/website/content/docs/middleware/guides/python-langgraph.mdx new file mode 100644 index 000000000..a4328d2f9 --- /dev/null +++ b/apps/website/content/docs/middleware/guides/python-langgraph.mdx @@ -0,0 +1,95 @@ +# Python LangGraph Middleware + +The Python package is `threadplane-middleware`. It is the Python LangGraph twin of `@threadplane/middleware/langgraph`: it binds browser-declared client-tool stubs onto a chat model and routes client-tool-only turns to `END`. + +## Install + +```bash +pip install threadplane-middleware +``` + +The package depends on `langchain-core>=0.3.0` and `langgraph>=0.3.0`. + +Install your model provider package separately, for example `langchain-openai` when using `ChatOpenAI`. + +## Bind tools per run + +Call `bind_client_tools()` inside your agent node. The browser sends the tool catalog with each run, so the model-visible tool list is request-scoped. + +```python +from langchain_openai import ChatOpenAI +from threadplane.middleware.langgraph import bind_client_tools + +SERVER_TOOLS = [search_tool] +base_llm = ChatOpenAI(model="gpt-4o-mini") + +def agent_node(state): + llm = bind_client_tools(base_llm, SERVER_TOOLS, state) + response = llm.invoke(state["messages"]) + return {"messages": [response]} +``` + +The helper reads `state["tools"]` first and falls back to `state["client_tools"]`. It appends each client tool as an explicit OpenAI function-tool dict: + +```python +{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Read local weather", + "parameters": {"type": "object"}, + }, +} +``` + +## Route after the agent + +Use `route_after_agent()` from a LangGraph conditional edge. It returns the server tools node name when the last model message contains a server or unknown tool call. It returns `__end__` when the turn has only client tool calls or no tool calls. + +```python +from langgraph.graph import END, StateGraph +from langgraph.prebuilt import ToolNode +from threadplane.middleware.langgraph import route_after_agent + +server_tool_names = [tool.name for tool in SERVER_TOOLS] + +def router(state): + return route_after_agent(state, server_tool_names) + +graph = StateGraph(...) +graph.add_node("agent", agent_node) +graph.add_node("tools", ToolNode(SERVER_TOOLS)) +graph.add_conditional_edges("agent", router, {"tools": "tools", "__end__": END}) +``` + +You can override the returned route labels: + +```python +route_after_agent(state, server_tool_names, tools_node="server_tools", end="done") +``` + +## Helper surface + +```python +from threadplane.middleware.langgraph import ( + bind_client_tools, + client_tool_specs, + client_tool_names, + has_client_tool_call, + has_server_tool_call, + last_message, + route_after_agent, +) +``` + +| Helper | Purpose | +|--------|---------| +| `client_tool_specs(state)` | Convert the run catalog into OpenAI function-tool dicts. | +| `client_tool_names(state)` | Return the set of client-declared tool names. | +| `has_client_tool_call(state)` | Check whether the last message calls a known client tool. | +| `has_server_tool_call(state, server_tool_names)` | Check whether the last message calls a server or unknown tool. | +| `last_message(state)` | Return the last message from `state["messages"]`, or `None`. | + +## Frontend contract + +The middleware does not execute browser tools. The frontend still needs to send the catalog, observe the model tool call, execute the local function or UI interaction, and resume the graph with a `ToolMessage` containing the result. diff --git a/apps/website/content/docs/telemetry/api/api-docs.json b/apps/website/content/docs/telemetry/api/api-docs.json index f47e20f7b..6bd68da4f 100644 --- a/apps/website/content/docs/telemetry/api/api-docs.json +++ b/apps/website/content/docs/telemetry/api/api-docs.json @@ -2,7 +2,7 @@ { "name": "ThreadplaneTelemetryService", "kind": "class", - "description": "", + "description": "Browser-side telemetry service.\n\nThe service no-ops unless `provideThreadplaneTelemetry({ enabled: true })`\nconfigured it. It enriches sent events with `sample_weight`, then delivers\nthem through `sink`, `endpoint`, or legacy PostHog configuration.", "params": [], "examples": [], "properties": [], @@ -10,7 +10,7 @@ { "name": "capture", "signature": "capture(event: ThreadplaneTelemetryEvent, properties: Record): Promise", - "description": "", + "description": "Capture an arbitrary enabled browser telemetry event.", "params": [ { "name": "event", @@ -29,7 +29,7 @@ { "name": "captureRuntimeInstanceCreated", "signature": "captureRuntimeInstanceCreated(input: ThreadplaneBrowserRuntimeTelemetry): Promise", - "description": "", + "description": "Capture a runtime construction event.", "params": [ { "name": "input", @@ -42,7 +42,7 @@ { "name": "captureRuntimeRequestCreated", "signature": "captureRuntimeRequestCreated(input: ThreadplaneBrowserRuntimeTelemetry & { requestType: string }): Promise", - "description": "", + "description": "Capture a runtime request creation event.", "params": [ { "name": "input", @@ -55,7 +55,7 @@ { "name": "captureStreamEnded", "signature": "captureStreamEnded(input: ThreadplaneBrowserStreamTelemetry): Promise", - "description": "", + "description": "Capture a stream-end event.", "params": [ { "name": "input", @@ -68,7 +68,7 @@ { "name": "captureStreamErrored", "signature": "captureStreamErrored(input: ThreadplaneBrowserStreamErrorTelemetry): Promise", - "description": "", + "description": "Capture a stream-error event, sending only the derived error class.", "params": [ { "name": "input", @@ -81,7 +81,7 @@ { "name": "captureStreamStarted", "signature": "captureStreamStarted(input: ThreadplaneBrowserStreamTelemetry): Promise", - "description": "", + "description": "Capture a stream-start event.", "params": [ { "name": "input", @@ -96,30 +96,30 @@ { "name": "ThreadplaneBrowserRuntimeTelemetry", "kind": "interface", - "description": "", + "description": "Runtime lifecycle properties captured from browser-side agent adapters.", "properties": [ { "name": "model", "type": "string", - "description": "", + "description": "Optional model name.", "optional": true }, { "name": "provider", "type": "string", - "description": "", + "description": "Optional model provider name.", "optional": true }, { "name": "surface", "type": "string", - "description": "", + "description": "Optional product or app surface tag.", "optional": true }, { "name": "transport", "type": "string", - "description": "", + "description": "Runtime transport, such as `langgraph` or `ag-ui`.", "optional": false } ], @@ -128,42 +128,42 @@ { "name": "ThreadplaneBrowserStreamErrorTelemetry", "kind": "interface", - "description": "", + "description": "Stream error telemetry. The raw error is reduced to an error class.", "properties": [ { "name": "durationMs", "type": "number", - "description": "", + "description": "Stream duration in milliseconds, when known.", "optional": true }, { "name": "error", "type": "unknown", - "description": "", + "description": "Raw error object or value; capture sends only `errorClass`.", "optional": true }, { "name": "model", "type": "string", - "description": "", + "description": "Optional model name.", "optional": true }, { "name": "provider", "type": "string", - "description": "", + "description": "Optional model provider name.", "optional": true }, { "name": "surface", "type": "string", - "description": "", + "description": "Optional product or app surface tag.", "optional": true }, { "name": "transport", "type": "string", - "description": "", + "description": "Runtime transport, such as `langgraph` or `ag-ui`.", "optional": false } ], @@ -172,36 +172,36 @@ { "name": "ThreadplaneBrowserStreamTelemetry", "kind": "interface", - "description": "", + "description": "Stream telemetry properties captured from browser-side agent adapters.", "properties": [ { "name": "durationMs", "type": "number", - "description": "", + "description": "Stream duration in milliseconds, when known.", "optional": true }, { "name": "model", "type": "string", - "description": "", + "description": "Optional model name.", "optional": true }, { "name": "provider", "type": "string", - "description": "", + "description": "Optional model provider name.", "optional": true }, { "name": "surface", "type": "string", - "description": "", + "description": "Optional product or app surface tag.", "optional": true }, { "name": "transport", "type": "string", - "description": "", + "description": "Runtime transport, such as `langgraph` or `ag-ui`.", "optional": false } ], @@ -328,7 +328,7 @@ { "name": "provideThreadplaneTelemetry", "kind": "function", - "description": "", + "description": "Provide browser telemetry configuration for an Angular app.\n\nBrowser telemetry remains off unless this provider is installed with\n`enabled: true`. Delivery goes through `sink`, `endpoint`, or the legacy\nPostHog options configured on `ThreadplaneTelemetryConfig`.", "signature": "provideThreadplaneTelemetry(config: ThreadplaneTelemetryConfig): EnvironmentProviders", "params": [ { diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index 0a1f3955a..67a764ef4 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -82,7 +82,7 @@ interface SupportingLib { const SUPPORTING: SupportingLib[] = [ { title: 'Middleware', - blurb: 'Backend client-tool routing', + blurb: 'JS & Python client-tool routing', href: '/docs/middleware/getting-started/introduction', glyph: 'middleware', }, diff --git a/apps/website/src/app/llms.txt/route.ts b/apps/website/src/app/llms.txt/route.ts index d7c6b43c8..aafd42464 100644 --- a/apps/website/src/app/llms.txt/route.ts +++ b/apps/website/src/app/llms.txt/route.ts @@ -26,7 +26,8 @@ function buildLlmsTxt(): string { '- @threadplane/ag-ui — adapter for any AG-UI-compatible backend (CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, AWS Strands, CopilotKit runtime)', '- @threadplane/render — generative UI runtime (Vercel json-render + Google A2UI)', '- @threadplane/a2ui — protocol types, JSONL parser, dynamic value resolver, and pointer helpers for A2UI streams', - '- @threadplane/middleware — backend LangGraph helpers for browser-executed client tools', + '- @threadplane/middleware — LangGraph.js helpers for browser-executed client tools', + '- threadplane-middleware — Python LangGraph helpers for browser-executed client tools', '- @threadplane/licensing — offline license verification and non-blocking package check helpers', '- @threadplane/telemetry — browser, Node, and shared telemetry helpers with privacy controls', '', @@ -35,6 +36,8 @@ function buildLlmsTxt(): string { 'npm install @threadplane/chat @threadplane/langgraph @langchain/core @langchain/langgraph-sdk marked', '# LangGraph backend with browser client tools:', 'npm install @threadplane/chat @threadplane/langgraph @langchain/core @langchain/langgraph-sdk @threadplane/middleware marked', + '# Python LangGraph backend with browser client tools:', + 'pip install threadplane-middleware', '# AG-UI backend:', 'npm install @threadplane/chat @threadplane/ag-ui @ag-ui/client @ag-ui/core marked', '', diff --git a/apps/website/src/lib/docs-config.ts b/apps/website/src/lib/docs-config.ts index a299e3d3d..f3dc85a15 100644 --- a/apps/website/src/lib/docs-config.ts +++ b/apps/website/src/lib/docs-config.ts @@ -357,7 +357,8 @@ export const docsConfig: DocsLibrary[] = [ id: 'guides', color: 'blue', pages: [ - { title: 'LangGraph Client Tools', slug: 'langgraph-client-tools', section: 'guides' }, + { title: 'LangGraph.js Client Tools', slug: 'langgraph-client-tools', section: 'guides' }, + { title: 'Python LangGraph Middleware', slug: 'python-langgraph', section: 'guides' }, ], }, { @@ -365,7 +366,7 @@ export const docsConfig: DocsLibrary[] = [ id: 'api', color: 'blue', pages: [ - { title: 'bindClientTools()', slug: 'client-tool-helpers', section: 'api' }, + { title: 'LangGraph.js Helpers', slug: 'client-tool-helpers', section: 'api' }, ], }, ], diff --git a/libs/licensing/src/lib/evaluate-license.ts b/libs/licensing/src/lib/evaluate-license.ts index 76c0443bf..5a9e8a386 100644 --- a/libs/licensing/src/lib/evaluate-license.ts +++ b/libs/licensing/src/lib/evaluate-license.ts @@ -27,6 +27,14 @@ export interface EvaluateResult { const FOURTEEN_DAYS_SEC = 14 * 24 * 60 * 60; +/** + * Convert a verified token result into a license status. + * + * `verifyResult` may be undefined when no token was supplied. In that case a + * caller-provided `isNoncommercial` hint returns `noncommercial`; otherwise the + * status is `missing`. Signature failures return `tampered`, and valid claims + * are evaluated against `exp` plus the optional grace window. + */ export function evaluateLicense( verifyResult: VerifyResult | undefined, options: EvaluateOptions, diff --git a/libs/licensing/src/lib/nag.ts b/libs/licensing/src/lib/nag.ts index 7807f3df1..954fdd771 100644 --- a/libs/licensing/src/lib/nag.ts +++ b/libs/licensing/src/lib/nag.ts @@ -21,6 +21,12 @@ const MESSAGES: Record = { 'license signature is invalid or malformed. Download a fresh key from https://threadplane.ai/pricing', }; +/** + * Emit one non-blocking license warning for a package/status pair. + * + * `licensed` and `noncommercial` statuses are silent. Other statuses warn once + * per package and status so repeated initialization does not spam logs. + */ export function emitNag( result: Pick, options: EmitNagOptions, diff --git a/libs/licensing/src/lib/run-license-check.ts b/libs/licensing/src/lib/run-license-check.ts index 243116558..15937bf98 100644 --- a/libs/licensing/src/lib/run-license-check.ts +++ b/libs/licensing/src/lib/run-license-check.ts @@ -20,6 +20,13 @@ export interface RunLicenseCheckOptions { const done = new Map(); +/** + * Run the full package license check. + * + * The helper verifies an optional token, evaluates the status, emits the + * package nag warning when appropriate, and memoizes identical package/token + * pairs so repeated provider initialization stays quiet. + */ export async function runLicenseCheck( options: RunLicenseCheckOptions, ): Promise { diff --git a/libs/telemetry/src/browser/provide.ts b/libs/telemetry/src/browser/provide.ts index d6d01320c..ad75adbf4 100644 --- a/libs/telemetry/src/browser/provide.ts +++ b/libs/telemetry/src/browser/provide.ts @@ -2,6 +2,13 @@ import { makeEnvironmentProviders, type EnvironmentProviders } from '@angular/co import { THREADPLANE_TELEMETRY_CONFIG, type ThreadplaneTelemetryConfig } from './tokens'; import { ThreadplaneTelemetryService } from './service'; +/** + * Provide browser telemetry configuration for an Angular app. + * + * Browser telemetry remains off unless this provider is installed with + * `enabled: true`. Delivery goes through `sink`, `endpoint`, or the legacy + * PostHog options configured on `ThreadplaneTelemetryConfig`. + */ export function provideThreadplaneTelemetry(config: ThreadplaneTelemetryConfig): EnvironmentProviders { return makeEnvironmentProviders([ { provide: THREADPLANE_TELEMETRY_CONFIG, useValue: config }, diff --git a/libs/telemetry/src/browser/service.ts b/libs/telemetry/src/browser/service.ts index 8cdcac12e..e709807ab 100644 --- a/libs/telemetry/src/browser/service.ts +++ b/libs/telemetry/src/browser/service.ts @@ -10,18 +10,27 @@ import { // Keep this type in sync with shared/events.ts. export type ThreadplaneBrowserEvent = ThreadplaneTelemetryEvent; +/** Runtime lifecycle properties captured from browser-side agent adapters. */ export interface ThreadplaneBrowserRuntimeTelemetry { + /** Runtime transport, such as `langgraph` or `ag-ui`. */ transport: string; + /** Optional product or app surface tag. */ surface?: string; + /** Optional model provider name. */ provider?: string; + /** Optional model name. */ model?: string; } +/** Stream telemetry properties captured from browser-side agent adapters. */ export interface ThreadplaneBrowserStreamTelemetry extends ThreadplaneBrowserRuntimeTelemetry { + /** Stream duration in milliseconds, when known. */ durationMs?: number; } +/** Stream error telemetry. The raw error is reduced to an error class. */ export interface ThreadplaneBrowserStreamErrorTelemetry extends ThreadplaneBrowserStreamTelemetry { + /** Raw error object or value; capture sends only `errorClass`. */ error?: unknown; } @@ -41,12 +50,20 @@ function errorClass(error: unknown): string { return 'UnknownError'; } +/** + * Browser-side telemetry service. + * + * The service no-ops unless `provideThreadplaneTelemetry({ enabled: true })` + * configured it. It enriches sent events with `sample_weight`, then delivers + * them through `sink`, `endpoint`, or legacy PostHog configuration. + */ @Injectable({ providedIn: 'root' }) export class ThreadplaneTelemetryService { private config: ThreadplaneTelemetryConfig | null = inject(THREADPLANE_TELEMETRY_CONFIG, { optional: true }); private postHogPromise: Promise | null = null; private distinctId: string | null = null; + /** Capture an arbitrary enabled browser telemetry event. */ async capture(event: ThreadplaneTelemetryEvent, properties?: Record): Promise { if (!this.config?.enabled) return; const sampleRate = normalizeSampleRate(this.config.sampleRate); @@ -76,22 +93,27 @@ export class ThreadplaneTelemetryService { } } + /** Capture a runtime construction event. */ captureRuntimeInstanceCreated(input: ThreadplaneBrowserRuntimeTelemetry): Promise { return this.capture('tplane:runtime_instance_created', { ...input }); } + /** Capture a runtime request creation event. */ captureRuntimeRequestCreated(input: ThreadplaneBrowserRuntimeTelemetry & { requestType: string }): Promise { return this.capture('tplane:runtime_request_created', { ...input }); } + /** Capture a stream-start event. */ captureStreamStarted(input: ThreadplaneBrowserStreamTelemetry): Promise { return this.capture('tplane:stream_started', { ...input }); } + /** Capture a stream-end event. */ captureStreamEnded(input: ThreadplaneBrowserStreamTelemetry): Promise { return this.capture('tplane:stream_ended', { ...input }); } + /** Capture a stream-error event, sending only the derived error class. */ captureStreamErrored(input: ThreadplaneBrowserStreamErrorTelemetry): Promise { const { error, ...rest } = input; return this.capture('tplane:stream_errored', { diff --git a/packages/threadplane-middleware/README.md b/packages/threadplane-middleware/README.md index 8a2ed021a..9cffdbcd3 100644 --- a/packages/threadplane-middleware/README.md +++ b/packages/threadplane-middleware/README.md @@ -3,6 +3,9 @@ LangGraph middleware for binding client-declared tool stubs and routing client tool calls to `END` so the browser executes them. +This is the Python LangGraph package. For LangGraph.js, use +`@threadplane/middleware` from npm and import `@threadplane/middleware/langgraph`. + ## How it works When a browser client sends a tool catalog (`{name, description, parameters}` @@ -72,6 +75,5 @@ from threadplane.middleware.langgraph import ( ```bash uv venv -uv pip install -e '.[test]' -uv run pytest -q +uv run --extra test python -m pytest -q ```