From f41cad9f3dd302b85c1c35afbd3abf22475d65df Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 19 Aug 2026 17:42:36 +0200 Subject: [PATCH 01/19] ref(v4)!: Remove flue pre-v1 support (#2380) ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) --- .changeset/remove-flue-pre-v1.md | 5 + e2e/config/pr-comment-scenarios.json | 8 - .../flue-instrumentation/.flue/app.mjs | 82 +- .../.flue/workflows/instrumentation.mjs | 182 +- .../__cassettes__/flue-v0-8-0.cassette.json | 3727 ----------- .../flue-v0-8-latest.cassette.json | 5859 ----------------- .../flue-v0-8-0-auto-hook.span-tree.json | 1448 ---- .../flue-v0-8-0-auto-hook.span-tree.txt | 1335 ---- .../flue-v0-8-0-cli.span-tree.json | 1448 ---- .../flue-v0-8-0-cli.span-tree.txt | 1335 ---- .../flue-v0-8-0-explicit.span-tree.json | 1448 ---- .../flue-v0-8-0-explicit.span-tree.txt | 1335 ---- .../flue-v0-8-latest-auto-hook.span-tree.json | 1662 ----- .../flue-v0-8-latest-auto-hook.span-tree.txt | 1499 ----- .../flue-v0-8-latest-cli.span-tree.json | 1662 ----- .../flue-v0-8-latest-cli.span-tree.txt | 1499 ----- .../flue-v0-8-latest-explicit.span-tree.json | 1662 ----- .../flue-v0-8-latest-explicit.span-tree.txt | 1499 ----- ...flue-v1-0-0-beta-3-explicit.span-tree.json | 4 +- .../flue-v1-0-0-beta-3-explicit.span-tree.txt | 4 +- .../flue-v1-latest-explicit.span-tree.json | 4 +- .../flue-v1-latest-explicit.span-tree.txt | 4 +- .../flue-instrumentation/assertions.ts | 142 +- .../flue-instrumentation/package.json | 20 +- .../flue-instrumentation/pnpm-lock.yaml | 364 +- .../flue-instrumentation/scenario.cli.mjs | 4 - .../flue-instrumentation/scenario.impl.mjs | 115 +- .../flue-instrumentation/scenario.mjs | 8 - .../flue-instrumentation/scenario.test.ts | 119 +- .../flue-instrumentation/scenario.ts | 4 +- .../versions/v1/.flue/app.mjs | 112 - .../v1/.flue/workflows/instrumentation.mjs | 183 - js/src/auto-instrumentations/configs/all.ts | 5 - .../configs/flue.test.ts | 63 - js/src/auto-instrumentations/configs/flue.ts | 19 - js/src/exports.ts | 5 +- js/src/instrumentation/braintrust-plugin.ts | 12 - js/src/instrumentation/config.ts | 4 - js/src/instrumentation/index.ts | 5 +- .../instrumentation/plugins/flue-channels.ts | 14 - .../plugins/flue-plugin.test.ts | 558 +- js/src/instrumentation/plugins/flue-plugin.ts | 361 +- .../plugins/instrumentation-names.test.ts | 2 - js/src/vendor-sdk-types/flue.ts | 70 +- 44 files changed, 364 insertions(+), 29536 deletions(-) create mode 100644 .changeset/remove-flue-pre-v1.md delete mode 100644 e2e/scenarios/flue-instrumentation/__cassettes__/flue-v0-8-0.cassette.json delete mode 100644 e2e/scenarios/flue-instrumentation/__cassettes__/flue-v0-8-latest.cassette.json delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.json delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.txt delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.json delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.txt delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.json delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.txt delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-auto-hook.span-tree.json delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-auto-hook.span-tree.txt delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-cli.span-tree.json delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-cli.span-tree.txt delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-explicit.span-tree.json delete mode 100644 e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-explicit.span-tree.txt delete mode 100644 e2e/scenarios/flue-instrumentation/scenario.cli.mjs delete mode 100644 e2e/scenarios/flue-instrumentation/scenario.mjs delete mode 100644 e2e/scenarios/flue-instrumentation/versions/v1/.flue/app.mjs delete mode 100644 e2e/scenarios/flue-instrumentation/versions/v1/.flue/workflows/instrumentation.mjs delete mode 100644 js/src/auto-instrumentations/configs/flue.test.ts delete mode 100644 js/src/auto-instrumentations/configs/flue.ts delete mode 100644 js/src/instrumentation/plugins/flue-channels.ts diff --git a/.changeset/remove-flue-pre-v1.md b/.changeset/remove-flue-pre-v1.md new file mode 100644 index 000000000..a029265f8 --- /dev/null +++ b/.changeset/remove-flue-pre-v1.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref(v4)!: Remove flue pre-v1 support diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index eb2b3f5f4..4399ab7e3 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -600,14 +600,6 @@ "label": "Flue Instrumentation", "metadataScenario": "flue-instrumentation", "variants": [ - { - "variantKey": "flue-v0-8-0", - "label": "v0.8 pinned" - }, - { - "variantKey": "flue-v0-8-latest", - "label": "v0.8 latest" - }, { "variantKey": "flue-v1-0-0-beta-3", "label": "v1 pinned" diff --git a/e2e/scenarios/flue-instrumentation/.flue/app.mjs b/e2e/scenarios/flue-instrumentation/.flue/app.mjs index 900402277..5d3ce1360 100644 --- a/e2e/scenarios/flue-instrumentation/.flue/app.mjs +++ b/e2e/scenarios/flue-instrumentation/.flue/app.mjs @@ -1,12 +1,12 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import { dirname } from "node:path"; -import { flush, initLogger } from "braintrust"; +import { braintrustFlueInstrumentation, flush, initLogger } from "braintrust"; +import { Hono } from "hono"; const runtimePackageName = process.env.FLUE_RUNTIME_PACKAGE_NAME ?? "@flue/runtime"; -const { configureProvider, flue, observe } = await import( - `${runtimePackageName}/app` -); +const [{ instrument, registerProvider }, { flue }] = await Promise.all([ + import(runtimePackageName), + import(`${runtimePackageName}/routing`), +]); function projectName() { const configured = process.env.BRAINTRUST_E2E_PROJECT_NAME; @@ -19,84 +19,24 @@ function projectName() { initLogger({ projectName: projectName() }); -const exitProcess = process.exit.bind(process); -if (process.env.FLUE_E2E_FLUSH_FILE) { - let isExiting = false; - process.exit = (code) => { - if (isExiting) { - return exitProcess(code); - } - isExiting = true; - const keepAlive = setTimeout(() => {}, 30_000); - void flushBeforeExit() - .catch((error) => { - console.error(error); - }) - .finally(() => { - clearTimeout(keepAlive); - exitProcess(code); - }); - }; -} - -if (process.env.FLUE_E2E_EXPLICIT_OBSERVE === "1") { - const { braintrustFlueObserver } = await import("braintrust"); - observe(braintrustFlueObserver); -} +instrument(braintrustFlueInstrumentation()); const openAIBaseUrl = process.env.OPENAI_BASE_URL ?? process.env.BRAINTRUST_E2E_MODEL_BASE_URL; if (openAIBaseUrl) { - configureProvider("openai", { baseUrl: openAIBaseUrl }); + registerProvider("openai", { baseUrl: openAIBaseUrl }); } const anthropicBaseUrl = process.env.ANTHROPIC_BASE_URL; if (anthropicBaseUrl) { - configureProvider("anthropic", { + registerProvider("anthropic", { apiKey: process.env.ANTHROPIC_API_KEY ?? "test-key", baseUrl: anthropicBaseUrl, }); } -let didScheduleFlush = false; -function scheduleFinalFlush(exitAfterFlush = false) { - if (didScheduleFlush) { - return; - } - didScheduleFlush = true; - const keepAlive = setTimeout(() => {}, 30_000); - void flushBeforeExit() - .catch((error) => { - console.error(error); - }) - .finally(() => { - clearTimeout(keepAlive); - if (exitAfterFlush) { - exitProcess(0); - } - }); -} - -process.on("SIGTERM", () => { - scheduleFinalFlush(true); -}); - -process.on("beforeExit", () => { - scheduleFinalFlush(); -}); - -const app = flue(); - -async function flushBeforeExit() { - await new Promise((resolve) => setTimeout(resolve, 250)); - await flush(); - if (process.env.FLUE_E2E_FLUSH_FILE) { - await mkdir(dirname(process.env.FLUE_E2E_FLUSH_FILE), { - recursive: true, - }); - await writeFile(process.env.FLUE_E2E_FLUSH_FILE, "ok"); - } -} +const app = new Hono(); +app.route("/", flue()); export default { async fetch(request, env, ctx) { diff --git a/e2e/scenarios/flue-instrumentation/.flue/workflows/instrumentation.mjs b/e2e/scenarios/flue-instrumentation/.flue/workflows/instrumentation.mjs index dd15a5516..509be2b53 100644 --- a/e2e/scenarios/flue-instrumentation/.flue/workflows/instrumentation.mjs +++ b/e2e/scenarios/flue-instrumentation/.flue/workflows/instrumentation.mjs @@ -1,4 +1,5 @@ import { traced } from "braintrust"; +import * as v from "valibot"; import { FLUE_MODEL, FLUE_REASONING_MODEL, @@ -7,10 +8,11 @@ import { const runtimePackageName = process.env.FLUE_RUNTIME_PACKAGE_NAME ?? "@flue/runtime"; -const [{ createAgent, Type }, { local }] = await Promise.all([ - import(runtimePackageName), - import(`${runtimePackageName}/node`), -]); +const [{ defineAgent, defineTool, defineWorkflow }, { local }] = + await Promise.all([ + import(runtimePackageName), + import(`${runtimePackageName}/node`), + ]); function flueModel() { return process.env.FLUE_E2E_MODEL ?? FLUE_MODEL; @@ -34,7 +36,7 @@ function flueReasoningThinkingLevel() { return process.env.FLUE_E2E_REASONING_THINKING_LEVEL ?? "medium"; } -const flueE2EAgent = createAgent(() => ({ +const flueE2EAgent = defineAgent(() => ({ compaction: { keepRecentTokens: 1, reserveTokens: 64, @@ -51,10 +53,14 @@ const flueE2EAgent = createAgent(() => ({ thinkingLevel: "off", })); -const lookupTool = { +const lookupTool = defineTool({ description: "Return a deterministic lookup result with an id needed by web_search.", - execute: async (args) => { + input: v.object({ + query: v.string(), + }), + name: "lookup", + run: async ({ input }) => { await traced( async (span) => { span.log({ output: "lookup-active" }); @@ -69,109 +75,109 @@ const lookupTool = { }, ); - return JSON.stringify({ + return { id: "flue-session-2026", - query: args.query, + query: input.query, topic: "session instrumentation", - }); + }; }, - name: "lookup", - parameters: Type.Object({ - query: Type.String(), - }), -}; +}); -const webSearchTool = { +const webSearchTool = defineTool({ description: "Search a deterministic local web index. Requires the id returned by lookup.", - execute: async (args) => - JSON.stringify({ - lookupId: args.lookupId, - query: args.query, - results: [ - { - title: "Flue reasoning stream instrumentation", - url: "https://example.test/flue/reasoning-streams", - }, - ], - }), + input: v.object({ + lookupId: v.string(), + query: v.string(), + }), name: "web_search", - parameters: Type.Object({ - lookupId: Type.String(), - query: Type.String(), + run: async ({ input }) => ({ + lookupId: input.lookupId, + query: input.query, + results: [ + { + title: "Flue reasoning stream instrumentation", + url: "https://example.test/flue/reasoning-streams", + }, + ], }), -}; +}); -const summarizeSourceTool = { +const summarizeSourceTool = defineTool({ description: "Summarize the selected deterministic source after web_search returns a URL.", - execute: async (args) => - JSON.stringify({ - summary: - "Flue emits reasoning, tool execution, and LLM turn events separately.", - url: args.url, - }), + input: v.object({ + url: v.string(), + }), name: "summarize_source", - parameters: Type.Object({ - url: Type.String(), + run: async ({ input }) => ({ + summary: + "Flue emits reasoning, tool execution, and LLM turn events separately.", + url: input.url, }), -}; +}); export async function route(_ctx, next) { await next(); } -export async function run({ init, payload }) { - await traced( - async (span) => { - span.log({ output: "active" }); - }, - { - name: "flue.workflowCurrentProbe", - event: { - metadata: { - scenario: SCENARIO_NAME, +export default defineWorkflow({ + agent: flueE2EAgent, + input: v.object({ + scenario: v.optional(v.string()), + metadata: v.optional(v.record(v.string(), v.unknown())), + }), + async run({ harness, input }) { + await traced( + async (span) => { + span.log({ output: "active" }); + }, + { + name: "flue.workflowCurrentProbe", + event: { + metadata: { + scenario: SCENARIO_NAME, + }, }, }, - }, - ); + ); - const harness = await init(flueE2EAgent, { name: "default" }); - const session = await harness.session("main"); - const skillSession = await harness.session("skill"); - const taskSession = await harness.session("task"); - - await session.prompt( - [ - "Complete this instrumented research flow.", - "Call exactly one tool per turn and wait for each tool result before choosing the next tool.", - 'Step 1: call lookup with query "flue instrumentation".', - 'Step 2: use the lookup result id as lookupId and call web_search with query "Braintrust Flue reasoning stream instrumentation".', - "Step 3: use the first web_search result url and call summarize_source.", - "After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - ].join(" "), - { - model: fluePromptModel(), - thinkingLevel: fluePromptThinkingLevel(), - tools: [lookupTool, webSearchTool, summarizeSourceTool], - }, - ); + const session = await harness.session("main"); + const skillSession = await harness.session("skill"); + const taskSession = await harness.session("task"); + + await session.prompt( + [ + "Complete this instrumented research flow.", + "Call exactly one tool per turn and wait for each tool result before choosing the next tool.", + 'Step 1: call lookup with query "flue instrumentation".', + 'Step 2: use the lookup result id as lookupId and call web_search with query "Braintrust Flue reasoning stream instrumentation".', + "Step 3: use the first web_search result url and call summarize_source.", + "After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", + ].join(" "), + { + model: fluePromptModel(), + thinkingLevel: fluePromptThinkingLevel(), + tools: [lookupTool, webSearchTool, summarizeSourceTool], + }, + ); - await skillSession.skill("e2e-flue-skill", { - args: { marker: "SKILL_DONE" }, - model: flueReasoningModel(), - thinkingLevel: "off", - }); + await skillSession.skill("e2e-flue-skill", { + args: { marker: "SKILL_DONE" }, + model: flueReasoningModel(), + thinkingLevel: "off", + }); - await taskSession.task("Reply with exactly TASK_DONE and no other text.", { - model: FLUE_MODEL, - thinkingLevel: "off", - }); + await taskSession.task("Reply with exactly TASK_DONE and no other text.", { + model: FLUE_MODEL, + thinkingLevel: "off", + }); - await session.compact(); + await session.compact(); - return { - scenario: payload?.scenario ?? SCENARIO_NAME, - status: "done", - }; -} + return { + scenario: input.scenario ?? SCENARIO_NAME, + status: "done", + }; + }, +}); diff --git a/e2e/scenarios/flue-instrumentation/__cassettes__/flue-v0-8-0.cassette.json b/e2e/scenarios/flue-instrumentation/__cassettes__/flue-v0-8-0.cassette.json deleted file mode 100644 index 996df5098..000000000 --- a/e2e/scenarios/flue-instrumentation/__cassettes__/flue-v0-8-0.cassette.json +++ /dev/null @@ -1,3727 +0,0 @@ -{ - "entries": [ - { - "callIndex": 0, - "id": "fcd9d51c145fb90b", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:40:49.578Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "budget_tokens": 8192, - "display": "summarized", - "type": "enabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - }, - { - "description": "Return a deterministic lookup result with an id needed by web_search.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "query": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "name": "lookup" - }, - { - "description": "Search a deterministic local web index. Requires the id returned by lookup.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "lookupId": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "required": ["lookupId", "query"], - "type": "object" - }, - "name": "web_search" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Summarize the selected deterministic source after web_search returns a URL.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "name": "summarize_source" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01A7K7rFFvpGEkYsKHme6rCx\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":10,\"cache_creation_input_tokens\":2222,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":2222,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":8,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The user wants me to complete a research\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" flow with exactly three steps:\\n1. Call lookup with query \\\"flue instrumentation\\\"\\n2. Call web_search with the\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" lookupId from step 1 and query \\\"Braintrust Flue reasoning stream instrumentation\\\"\\n3. Call summarize_source with the first URL from step 2\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n4. Reply with exactly \\\"PROMPT_DONE\\\"\\n\\nThe user emphasizes I should call exactly one tool per turn and wait for each result\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". Let me start with step 1.\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"I'll complete this instrumented research flow step\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\" by step. Starting with Step 1:\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":2,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01MbotvqKgQV4ecxQ566vyuC\",\"name\":\"lookup\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"query\\\": \\\"flue instrumentation\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":2}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":10,\"cache_creation_input_tokens\":2222,\"cache_read_input_tokens\":0,\"output_tokens\":196,\"output_tokens_details\":{\"thinking_tokens\":120}}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2999000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:40:45Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:40:45Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:40:45Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3599000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:40:45Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dac490ea0c301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:40:47 GMT", - "request-id": "req_011CbV4Yrzdciu7aM7XyPJvu", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-def621f99a586b9f40e35c15380d1f90-62267612b2e498ea-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "1355", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 1, - "id": "ff22a182ea5a9c06", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:40:53.810Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "signature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "id": "toolu_01MbotvqKgQV4ecxQ566vyuC", - "input": { - "query": "flue instrumentation" - }, - "name": "lookup", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "content": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "is_error": false, - "tool_use_id": "toolu_01MbotvqKgQV4ecxQ566vyuC", - "type": "tool_result" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "budget_tokens": 8192, - "display": "summarized", - "type": "enabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - }, - { - "description": "Return a deterministic lookup result with an id needed by web_search.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "query": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "name": "lookup" - }, - { - "description": "Search a deterministic local web index. Requires the id returned by lookup.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "lookupId": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "required": ["lookupId", "query"], - "type": "object" - }, - "name": "web_search" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Summarize the selected deterministic source after web_search returns a URL.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "name": "summarize_source" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01HZbhxuwLjyFGr1czSAWHTh\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":13,\"cache_creation_input_tokens\":236,\"cache_read_input_tokens\":2222,\"cache_creation\":{\"ephemeral_5m_input_tokens\":236,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Good\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\", I got the lookup result with id \\\"flue-session-2026\\\". Now I need to proceed to step 2:\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" call web_search with this lookupId and the query \\\"Braintrust Flue reasoning stream instrumentation\\\".\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"Now proceeding to Step 2:\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":2,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_018rc1ig7vXeron3rLfpUtRT\",\"name\":\"web_search\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"lookupId\\\": \\\"flue-session-2026\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"query\\\": \\\"Braintrust Flue reasoning stream instru\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"mentation\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":2}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":13,\"cache_creation_input_tokens\":236,\"cache_read_input_tokens\":2222,\"output_tokens\":158,\"output_tokens_details\":{\"thinking_tokens\":59}}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2999000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:40:49Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:40:49Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:40:49Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3599000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:40:49Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dac631dbac301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:40:52 GMT", - "request-id": "req_011CbV4ZAqRr9QtjYnVyY69n", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-68d6456f3077bdeb0822f73c39a51d2d-5be926a484ba813d-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "2225", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 2, - "id": "f1d50a1e6c760d6c", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:40:58.541Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "signature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "id": "toolu_01MbotvqKgQV4ecxQ566vyuC", - "input": { - "query": "flue instrumentation" - }, - "name": "lookup", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "is_error": false, - "tool_use_id": "toolu_01MbotvqKgQV4ecxQ566vyuC", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "signature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 2:", - "type": "text" - }, - { - "id": "toolu_018rc1ig7vXeron3rLfpUtRT", - "input": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "name": "web_search", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "content": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "is_error": false, - "tool_use_id": "toolu_018rc1ig7vXeron3rLfpUtRT", - "type": "tool_result" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "budget_tokens": 8192, - "display": "summarized", - "type": "enabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - }, - { - "description": "Return a deterministic lookup result with an id needed by web_search.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "query": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "name": "lookup" - }, - { - "description": "Search a deterministic local web index. Requires the id returned by lookup.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "lookupId": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "required": ["lookupId", "query"], - "type": "object" - }, - "name": "web_search" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Summarize the selected deterministic source after web_search returns a URL.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "name": "summarize_source" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01HJoo9ncrvNPVcaq2UWN45L\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":13,\"cache_creation_input_tokens\":234,\"cache_read_input_tokens\":2458,\"cache_creation\":{\"ephemeral_5m_input_tokens\":234,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":4,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Perfect! I got\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" the web_search results with one result. The first (and only) result has the URL\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" \\\"https://example.test/flue/reasoning-streams\\\". Now I need to proceed to step 3: call summarize_source with this URL.\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"Now proceeding to Step 3:\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":2,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01MRjt3UmRqrUYHt1ZC8gND9\",\"name\":\"summarize_source\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"url\\\": \\\"https://example.test/flue/reasoning-streams\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":2}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":13,\"cache_creation_input_tokens\":234,\"cache_read_input_tokens\":2458,\"output_tokens\":138,\"output_tokens_details\":{\"thinking_tokens\":61}}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2998000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:40:54Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:40:54Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:40:54Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3598000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:40:54Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dac7d4a1fc301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:40:56 GMT", - "request-id": "req_011CbV4ZUiiFpwboN3YtdzkH", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-6392939f911bfe9568c361c78254c370-cb7c6141cf28bb77-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "2757", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 3, - "id": "305957222aa1f6e9", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:41:01.029Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "signature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "id": "toolu_01MbotvqKgQV4ecxQ566vyuC", - "input": { - "query": "flue instrumentation" - }, - "name": "lookup", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "is_error": false, - "tool_use_id": "toolu_01MbotvqKgQV4ecxQ566vyuC", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "signature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 2:", - "type": "text" - }, - { - "id": "toolu_018rc1ig7vXeron3rLfpUtRT", - "input": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "name": "web_search", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "is_error": false, - "tool_use_id": "toolu_018rc1ig7vXeron3rLfpUtRT", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "signature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "id": "toolu_01MRjt3UmRqrUYHt1ZC8gND9", - "input": { - "url": "https://example.test/flue/reasoning-streams" - }, - "name": "summarize_source", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "content": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "is_error": false, - "tool_use_id": "toolu_01MRjt3UmRqrUYHt1ZC8gND9", - "type": "tool_result" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "budget_tokens": 8192, - "display": "summarized", - "type": "enabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - }, - { - "description": "Return a deterministic lookup result with an id needed by web_search.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "query": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "name": "lookup" - }, - { - "description": "Search a deterministic local web index. Requires the id returned by lookup.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "lookupId": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "required": ["lookupId", "query"], - "type": "object" - }, - "name": "web_search" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Summarize the selected deterministic source after web_search returns a URL.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "name": "summarize_source" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_019YC1YEtmEwfw9dyuY3MPdx\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":13,\"cache_creation_input_tokens\":194,\"cache_read_input_tokens\":2692,\"cache_creation\":{\"ephemeral_5m_input_tokens\":194,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Perfect\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"! The summarize_source call has completed successfully. According to the instructions, I should now reply with exactly \\\"PROMPT_DONE\\\" and no other text.\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"Eu8CCm4IDhgCKkAn2QOIIlYfvcs6ynsmhCslHDtBsrTA4CfmimK7zyz6+k6dZldV2JBtEBET41It0gFcGEKC0n0mAhaYVuLutpDDMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIM1xfLptxSF8rKzou8GgwRLuVmfiBYOT4CZIsiMAYz8TpvchMAWodP+T9uZ+dJ47MVdNv+p86nZXmT5d5EWlCroiAOFHHzJH2o0UjJQSquAX6wu+jIC8/DXwurGIIf2ToldPuJWF5HGj8kHMnwh777XS1NUFl1irM24rvP7kTAv3Dp8YEwb8t3g1EsicY7j+C5DzgwxVsPYCVOCgdMqQjtsqpL7AX5KQ9+hR1gK1DQjshv4Gze3hw/+C+9iptd3RlDIuwO0cm8NY+sVqeWVh3TtQujQlVt1rW/HfWjclrtT0LZSGwc6RypmfMMQi6lncIk/EPgy0XbS1plKW0tLBgB\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"PROMPT_DONE\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":13,\"cache_creation_input_tokens\":194,\"cache_read_input_tokens\":2692,\"output_tokens\":51,\"output_tokens_details\":{\"thinking_tokens\":40}}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2998000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:40:58Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:40:58Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:40:58Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3598000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:40:58Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dac9afd8fc301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:00 GMT", - "request-id": "req_011CbV4Zq3L7gJxP1Y46qapx", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-e0b4a5e0f6b8f2625262dec81a52e61e-c551de5bb2f6f203-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "1421", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 4, - "id": "3d93fe37f72a947f", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:41:05.303Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "type": "disabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01SoNm9ntxkXQkRrBSidrXSB\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":670,\"cache_read_input_tokens\":1099,\"cache_creation\":{\"ephemeral_5m_input_tokens\":670,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":5,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"I'll run the e\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"2e-flue-skill. Let me first read the skill file to understand its instructions.\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_0116y5v4MmMkEaDkjkEPPjTH\",\"name\":\"read\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\": \\\".agents/e2e-flue\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"-skill.md\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":670,\"cache_read_input_tokens\":1099,\"output_tokens\":89}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2999000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:41:01Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:41:01Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:41:01Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3599000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:41:01Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dacab0e1fc301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:04 GMT", - "request-id": "req_011CbV4a2cMY5XByBTCzUoHh", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-e05c9702b7456a4b1c655ed13d92ba42-b48b861130669b0c-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "2864", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 5, - "id": "fb4ed2bb1569d980", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:41:08.068Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "input": { - "path": ".agents/e2e-flue-skill.md" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "content": "ENOENT: no such file or directory, open '/Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "is_error": true, - "tool_use_id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "type": "tool_result" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "type": "disabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01HWMEiz1LLcSzQG6KkyGVxZ\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":7,\"cache_creation_input_tokens\":183,\"cache_read_input_tokens\":1769,\"cache_creation\":{\"ephemeral_5m_input_tokens\":183,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":52,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01VLK49ybvQa2MZoZ8FxbJCx\",\"name\":\"read\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\": \\\".agents\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":7,\"cache_creation_input_tokens\":183,\"cache_read_input_tokens\":1769,\"output_tokens\":52}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2999000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:41:05Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:41:05Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:41:05Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3599000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:41:05Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dacc65c8dc301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:07 GMT", - "request-id": "req_011CbV4aLhY55XXCvF766Z84", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-1467e6f052128317c79e4dcdcc34c0e2-95e633d304e0c3ad-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "2046", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 6, - "id": "2d5b5e2519b5661b", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:41:10.832Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "input": { - "path": ".agents/e2e-flue-skill.md" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "ENOENT: no such file or directory, open '/Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "is_error": true, - "tool_use_id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_01VLK49ybvQa2MZoZ8FxbJCx", - "input": { - "path": ".agents" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "content": "skills", - "is_error": false, - "tool_use_id": "toolu_01VLK49ybvQa2MZoZ8FxbJCx", - "type": "tool_result" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "type": "disabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01RRobMzKrtpPcHZAGQrRZCo\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":6,\"cache_creation_input_tokens\":66,\"cache_read_input_tokens\":1952,\"cache_creation\":{\"ephemeral_5m_input_tokens\":66,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":54,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_018pSd7nKSm3kfi2DVmzQPsA\",\"name\":\"read\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\": \\\".agents/skills\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":6,\"cache_creation_input_tokens\":66,\"cache_read_input_tokens\":1952,\"output_tokens\":54}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2999000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:41:08Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:41:08Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:41:08Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3599000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:41:08Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dacd76bc8c301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:10 GMT", - "request-id": "req_011CbV4aYPzoGb5kYRUcefKG", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-eb3b7c53373bb1de39b3c714cb8004f5-e91254d86c0e7b64-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "2024", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 7, - "id": "2cbc017f3c17c05f", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:41:13.081Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "input": { - "path": ".agents/e2e-flue-skill.md" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "ENOENT: no such file or directory, open '/Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "is_error": true, - "tool_use_id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_01VLK49ybvQa2MZoZ8FxbJCx", - "input": { - "path": ".agents" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "skills", - "is_error": false, - "tool_use_id": "toolu_01VLK49ybvQa2MZoZ8FxbJCx", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_018pSd7nKSm3kfi2DVmzQPsA", - "input": { - "path": ".agents/skills" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "content": "e2e-flue-skill", - "is_error": false, - "tool_use_id": "toolu_018pSd7nKSm3kfi2DVmzQPsA", - "type": "tool_result" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "type": "disabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01HaDyq5MaZpVHgVGhHJLmvs\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":6,\"cache_creation_input_tokens\":74,\"cache_read_input_tokens\":2018,\"cache_creation\":{\"ephemeral_5m_input_tokens\":74,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":63,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_012TAbdNbuuFaDw1naUg1vbT\",\"name\":\"read\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\": \\\".agents/skills/e2e-flue-skill\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":6,\"cache_creation_input_tokens\":74,\"cache_read_input_tokens\":2018,\"output_tokens\":63}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2999000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:41:11Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:41:11Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:41:11Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3599000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:41:11Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dace85ad2c301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:13 GMT", - "request-id": "req_011CbV4ajy1g4mH27mS5Ju8N", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-93ed38ba66b829c67909054d6945c9cb-af18094065e38879-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "1778", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 8, - "id": "4b885efa84390a1f", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:41:15.608Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "input": { - "path": ".agents/e2e-flue-skill.md" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "ENOENT: no such file or directory, open '/Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "is_error": true, - "tool_use_id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_01VLK49ybvQa2MZoZ8FxbJCx", - "input": { - "path": ".agents" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "skills", - "is_error": false, - "tool_use_id": "toolu_01VLK49ybvQa2MZoZ8FxbJCx", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_018pSd7nKSm3kfi2DVmzQPsA", - "input": { - "path": ".agents/skills" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "e2e-flue-skill", - "is_error": false, - "tool_use_id": "toolu_018pSd7nKSm3kfi2DVmzQPsA", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_012TAbdNbuuFaDw1naUg1vbT", - "input": { - "path": ".agents/skills/e2e-flue-skill" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "content": "SKILL.md", - "is_error": false, - "tool_use_id": "toolu_012TAbdNbuuFaDw1naUg1vbT", - "type": "tool_result" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "type": "disabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01FCY15E5PvgZ9MKVwMLQfmY\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":6,\"cache_creation_input_tokens\":79,\"cache_read_input_tokens\":2092,\"cache_creation\":{\"ephemeral_5m_input_tokens\":79,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":66,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01WQVh4vmLxZQhqpoXRvUi68\",\"name\":\"read\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\": \\\".agents/skills/e2e-flue-skill/SKILL.md\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":6,\"cache_creation_input_tokens\":79,\"cache_read_input_tokens\":2092,\"output_tokens\":68}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2999000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:41:13Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:41:13Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:41:13Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3599000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:41:13Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dacf599b5c301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:15 GMT", - "request-id": "req_011CbV4atzVFuefiAGaQnZig", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-75d6c58deb0a99da3a3ea03db1ae3fd9-01d8b31ff753a269-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "2118", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 9, - "id": "05dcf3cc09c5aa27", - "matchKey": "POST api.anthropic.com/v1/messages", - "recordedAt": "2026-05-28T13:41:17.354Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 64000, - "messages": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "input": { - "path": ".agents/e2e-flue-skill.md" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "ENOENT: no such file or directory, open '/Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "is_error": true, - "tool_use_id": "toolu_0116y5v4MmMkEaDkjkEPPjTH", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_01VLK49ybvQa2MZoZ8FxbJCx", - "input": { - "path": ".agents" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "skills", - "is_error": false, - "tool_use_id": "toolu_01VLK49ybvQa2MZoZ8FxbJCx", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_018pSd7nKSm3kfi2DVmzQPsA", - "input": { - "path": ".agents/skills" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "e2e-flue-skill", - "is_error": false, - "tool_use_id": "toolu_018pSd7nKSm3kfi2DVmzQPsA", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_012TAbdNbuuFaDw1naUg1vbT", - "input": { - "path": ".agents/skills/e2e-flue-skill" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "content": "SKILL.md", - "is_error": false, - "tool_use_id": "toolu_012TAbdNbuuFaDw1naUg1vbT", - "type": "tool_result" - } - ], - "role": "user" - }, - { - "content": [ - { - "id": "toolu_01WQVh4vmLxZQhqpoXRvUi68", - "input": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "name": "read", - "type": "tool_use" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "cache_control": { - "type": "ephemeral" - }, - "content": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "is_error": false, - "tool_use_id": "toolu_01WQVh4vmLxZQhqpoXRvUi68", - "type": "tool_result" - } - ], - "role": "user" - } - ], - "model": "claude-sonnet-4-5-20250929", - "stream": true, - "system": [ - { - "cache_control": { - "type": "ephemeral" - }, - "text": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "type": "text" - } - ], - "thinking": { - "type": "disabled" - }, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "name": "read" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "name": "write" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "name": "edit" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "name": "bash" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "grep" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "name": "glob" - }, - { - "cache_control": { - "type": "ephemeral" - }, - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "eager_input_streaming": true, - "input_schema": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "name": "task" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.anthropic.com/v1/messages" - }, - "response": { - "body": { - "chunks": [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01F7J8rewVTLnDMtxDcVdkiQ\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":6,\"cache_creation_input_tokens\":135,\"cache_read_input_tokens\":2171,\"cache_creation\":{\"ephemeral_5m_input_tokens\":135,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":3,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}}", - "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}", - "event: ping\ndata: {\"type\":\"ping\"}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"SKILL_\"}}", - "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"DONE\"}}", - "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}", - "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":6,\"cache_creation_input_tokens\":135,\"cache_read_input_tokens\":2171,\"output_tokens\":8}}", - "event: message_stop\ndata: {\"type\":\"message_stop\"}" - ], - "kind": "sse" - }, - "headers": { - "anthropic-organization-id": "27796668-7351-40ac-acc4-024aee8995a5", - "anthropic-ratelimit-input-tokens-limit": "3000000", - "anthropic-ratelimit-input-tokens-remaining": "2999000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-28T13:41:15Z", - "anthropic-ratelimit-output-tokens-limit": "600000", - "anthropic-ratelimit-output-tokens-remaining": "600000", - "anthropic-ratelimit-output-tokens-reset": "2026-05-28T13:41:15Z", - "anthropic-ratelimit-requests-limit": "20000", - "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-05-28T13:41:15Z", - "anthropic-ratelimit-tokens-limit": "3600000", - "anthropic-ratelimit-tokens-remaining": "3599000", - "anthropic-ratelimit-tokens-reset": "2026-05-28T13:41:15Z", - "cache-control": "no-cache", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dad05abc5c301-VIE", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-security-policy": "default-src 'none'; frame-ancestors 'none'", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:17 GMT", - "request-id": "req_011CbV4b62WtkEAN1uuw6ZTa", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-a95abb0aa0dc3ddebd7bd3e84f6431b6-69727f41ec2a9c74-01", - "transfer-encoding": "chunked", - "vary": "Accept-Encoding", - "x-envoy-upstream-service-time": "1258", - "x-robots-tag": "none" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 0, - "id": "7c0e162d6794cf5d", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-05-28T13:41:20.282Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Thu, May 28, 2026\nWorking directory: /Users/lucaforstner/conductor/workspaces/braintrust-sdk-javascript/shanghai-v1/e2e/.bt-tmp/run-u9uXnv/scenarios/flue-instrumentation\n\nDirectory structure:\n.agents\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts", - "role": "system" - }, - { - "content": [ - { - "text": "Reply with exactly TASK_DONE and no other text.", - "type": "input_text" - } - ], - "role": "user" - } - ], - "model": "gpt-4o-mini", - "prompt_cache_key": "[REDACTED]", - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_09b32b2821e9a4d8016a1845ff2ea88192a734cf6ba6abcb9d\",\"object\":\"response\",\"created_at\":1779975679,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"flue-e2e-instance::default::task:task:4cfd6e11-bac1-4736-9b87-b0\",\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_09b32b2821e9a4d8016a1845ff2ea88192a734cf6ba6abcb9d\",\"object\":\"response\",\"created_at\":1779975679,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"flue-e2e-instance::default::task:task:4cfd6e11-bac1-4736-9b87-b0\",\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"TASK\",\"item_id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\",\"logprobs\":[],\"obfuscation\":\"pyb2YZ3HkAzp\",\"output_index\":0,\"sequence_number\":4}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"_DONE\",\"item_id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\",\"logprobs\":[],\"obfuscation\":\"zDbKM0sYFZQ\",\"output_index\":0,\"sequence_number\":5}", - "event: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":6,\"text\":\"TASK_DONE\"}", - "event: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"TASK_DONE\"},\"sequence_number\":7}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"TASK_DONE\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":8}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_09b32b2821e9a4d8016a1845ff2ea88192a734cf6ba6abcb9d\",\"object\":\"response\",\"created_at\":1779975679,\"status\":\"completed\",\"background\":false,\"completed_at\":1779975680,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"TASK_DONE\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"flue-e2e-instance::default::task:task:4cfd6e11-bac1-4736-9b87-b0\",\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":804,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":4,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":808},\"user\":null,\"metadata\":{}},\"sequence_number\":9}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dad1668335adb-VIE", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:19 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "321", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-request-id": "req_fdef3765899a4c91bc975a65974dd9bb" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 1, - "id": "f12aff3913439248", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-05-28T13:41:21.738Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.", - "role": "system" - }, - { - "content": [ - { - "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant thinking]: The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.\n\n[Assistant]: I'll complete this instrumented research flow step by step. Starting with Step 1:\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant thinking]: Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".\n\n[Assistant]: Now proceeding to Step 2:\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant thinking]: Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.\n\n[Assistant]: Now proceeding to Step 3:\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", - "type": "input_text" - } - ], - "role": "user" - } - ], - "max_output_tokens": 32, - "model": "gpt-4o-mini", - "store": false, - "stream": true - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_01ecf68325f9e202016a184600a16881a08ded2a1961eda9f9\",\"object\":\"response\",\"created_at\":1779975680,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":32,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_01ecf68325f9e202016a184600a16881a08ded2a1961eda9f9\",\"object\":\"response\",\"created_at\":1779975680,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":32,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"##\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"d6v2dmzPZ7YChr\",\"output_index\":0,\"sequence_number\":4}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Original\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"AQ3m9tJ\",\"output_index\":0,\"sequence_number\":5}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Request\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"UKhv1LKl\",\"output_index\":0,\"sequence_number\":6}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"\\n\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"5muwgiakPkjQduY\",\"output_index\":0,\"sequence_number\":7}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"The\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"gYHJ7xqWx893Q\",\"output_index\":0,\"sequence_number\":8}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" user\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"ta2Vk95LgSS\",\"output_index\":0,\"sequence_number\":9}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" requested\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"T18OOl\",\"output_index\":0,\"sequence_number\":10}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" to\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"CgWTkMgs6kW4s\",\"output_index\":0,\"sequence_number\":11}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" complete\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"PQ7u3KE\",\"output_index\":0,\"sequence_number\":12}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" an\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"fyFt8NlmxkL8T\",\"output_index\":0,\"sequence_number\":13}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" instrument\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"idBTA\",\"output_index\":0,\"sequence_number\":14}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ed\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"Jqp8Abx3NlEpYO\",\"output_index\":0,\"sequence_number\":15}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" research\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"2Z7W3UL\",\"output_index\":0,\"sequence_number\":16}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" flow\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"CWcmUwdOK0I\",\"output_index\":0,\"sequence_number\":17}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" consisting\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"5A8K0\",\"output_index\":0,\"sequence_number\":18}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" of\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"8mUUf5KvarpcJ\",\"output_index\":0,\"sequence_number\":19}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" three\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"RpVzByEpb2\",\"output_index\":0,\"sequence_number\":20}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" specific\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"9qbTunN\",\"output_index\":0,\"sequence_number\":21}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" steps\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"sRljhC5wZy\",\"output_index\":0,\"sequence_number\":22}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\",\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"LQiEbRM4fizUNQm\",\"output_index\":0,\"sequence_number\":23}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" involving\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"CFyfLz\",\"output_index\":0,\"sequence_number\":24}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" calling\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"E6cI70HR\",\"output_index\":0,\"sequence_number\":25}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" various\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"GFRuEgJV\",\"output_index\":0,\"sequence_number\":26}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" tools\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"X4Ttj2uHGX\",\"output_index\":0,\"sequence_number\":27}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" in\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"9vlDZO40VZwFr\",\"output_index\":0,\"sequence_number\":28}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" a\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"tWdPAInHFKDip7\",\"output_index\":0,\"sequence_number\":29}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" sequential\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"Rct1b\",\"output_index\":0,\"sequence_number\":30}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" manner\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"0x4W3URIt\",\"output_index\":0,\"sequence_number\":31}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\\n\\n\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"EscdvHRY9LVkB\",\"output_index\":0,\"sequence_number\":32}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"##\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"7fE3Nivp8RY0eK\",\"output_index\":0,\"sequence_number\":33}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Early\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"qsGDoylwsO\",\"output_index\":0,\"sequence_number\":34}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Progress\",\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"obfuscation\":\"WxaVUkk\",\"output_index\":0,\"sequence_number\":35}", - "event: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":36,\"text\":\"## Original Request\\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\\n\\n## Early Progress\"}", - "event: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"## Original Request\\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\\n\\n## Early Progress\"},\"sequence_number\":37}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"type\":\"message\",\"status\":\"incomplete\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"## Original Request\\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\\n\\n## Early Progress\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":38}", - "event: response.incomplete\ndata: {\"type\":\"response.incomplete\",\"response\":{\"id\":\"resp_01ecf68325f9e202016a184600a16881a08ded2a1961eda9f9\",\"object\":\"response\",\"created_at\":1779975680,\"status\":\"incomplete\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":{\"reason\":\"max_output_tokens\"},\"instructions\":null,\"max_output_tokens\":32,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\",\"type\":\"message\",\"status\":\"incomplete\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"## Original Request\\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\\n\\n## Early Progress\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":694,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":32,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":726},\"user\":null,\"metadata\":{}},\"sequence_number\":39}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a02dad234c015adb-VIE", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Thu, 28 May 2026 13:41:20 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "90", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-request-id": "req_2bb8f213d8be45a8a8c6368e34e80335" - }, - "status": 200, - "statusText": "OK" - } - } - ], - "meta": { - "createdAt": "2026-05-28T13:01:46.277Z" - } -} diff --git a/e2e/scenarios/flue-instrumentation/__cassettes__/flue-v0-8-latest.cassette.json b/e2e/scenarios/flue-instrumentation/__cassettes__/flue-v0-8-latest.cassette.json deleted file mode 100644 index 69d4e8bd6..000000000 --- a/e2e/scenarios/flue-instrumentation/__cassettes__/flue-v0-8-latest.cassette.json +++ /dev/null @@ -1,5859 +0,0 @@ -{ - "entries": [ - { - "callIndex": 0, - "id": "7c0e162d6794cf5d", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:00.374Z", - "request": { - "body": { - "kind": "json", - "value": { - "include": ["reasoning.encrypted_content"], - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "input_text" - } - ], - "role": "user" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "low", - "summary": "auto" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Return a deterministic lookup result with an id needed by web_search.", - "name": "lookup", - "parameters": { - "properties": { - "query": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search a deterministic local web index. Requires the id returned by lookup.", - "name": "web_search", - "parameters": { - "properties": { - "lookupId": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "required": ["lookupId", "query"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Summarize the selected deterministic source after web_search returns a URL.", - "name": "summarize_source", - "parameters": { - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_045458052dc10379016a705b5f8ed48191ac5f4bcf9d7df429\",\"object\":\"response\",\"created_at\":1785748319,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_045458052dc10379016a705b5f8ed48191ac5f4bcf9d7df429\",\"object\":\"response\",\"created_at\":1785748319,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_rBgonKxWHwhN9E8aQ4VobIOe\",\"name\":\"lookup\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"obfuscation\":\"FJ9TowjZDi2V7k\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"query\",\"item_id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"obfuscation\":\"s6VEwiE9UsM\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"obfuscation\":\"6aWQMwZgQPiRf\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"fl\",\"item_id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"obfuscation\":\"KUoAPUAlQ7cmX9\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"obfuscation\":\"lQUgpuDVqWyLbo\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" instrumentation\",\"item_id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"obfuscation\":\"\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"obfuscation\":\"jkQ7s9o73nxqwB\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"query\\\":\\\"flue instrumentation\\\"}\",\"item_id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"output_index\":0,\"sequence_number\":10}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"query\\\":\\\"flue instrumentation\\\"}\",\"call_id\":\"call_rBgonKxWHwhN9E8aQ4VobIOe\",\"name\":\"lookup\"},\"output_index\":0,\"sequence_number\":11}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_045458052dc10379016a705b5f8ed48191ac5f4bcf9d7df429\",\"object\":\"response\",\"created_at\":1785748319,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748320,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_045458052dc10379016a705b6008408191b27653755cf5bd03\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"query\\\":\\\"flue instrumentation\\\"}\",\"call_id\":\"call_rBgonKxWHwhN9E8aQ4VobIOe\",\"name\":\"lookup\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1005,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":19,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1024},\"user\":null,\"metadata\":{}},\"sequence_number\":12}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432b38d617bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:11:59 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "572", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998539", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_3ad47b9556ec41a8bf046bb73633b660" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 1, - "id": "f12aff3913439248", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:01.038Z", - "request": { - "body": { - "kind": "json", - "value": { - "include": ["reasoning.encrypted_content"], - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"query\":\"flue instrumentation\"}", - "call_id": "call_rBgonKxWHwhN9E8aQ4VobIOe", - "id": "fc_045458052dc10379016a705b6008408191b27653755cf5bd03", - "name": "lookup", - "type": "function_call" - }, - { - "call_id": "call_rBgonKxWHwhN9E8aQ4VobIOe", - "output": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "low", - "summary": "auto" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Return a deterministic lookup result with an id needed by web_search.", - "name": "lookup", - "parameters": { - "properties": { - "query": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search a deterministic local web index. Requires the id returned by lookup.", - "name": "web_search", - "parameters": { - "properties": { - "lookupId": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "required": ["lookupId", "query"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Summarize the selected deterministic source after web_search returns a URL.", - "name": "summarize_source", - "parameters": { - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_045458052dc10379016a705b6070c48191b73ad8a8f9b550f7\",\"object\":\"response\",\"created_at\":1785748320,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_045458052dc10379016a705b6070c48191b73ad8a8f9b550f7\",\"object\":\"response\",\"created_at\":1785748320,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_fGesVCba2aABMoxSNfVnDVnR\",\"name\":\"web_search\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"CXnKLapUymev31\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"lookup\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"hPsgf6WVxe\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Id\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"jPh8SwIrBbqksW\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"IlU35DSXGkesV\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"fl\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"1XJ1BKxdLr8xZ1\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"1OTUMDyCdYkVv8\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-session\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"Nuz4FnUt\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"HwzHAjBygsfiSUX\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"202\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"Yp4H7ZJmIH7Dv\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"6\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"EYhWYsmGpl4z0l3\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"8RFEMjEMC5stu\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"query\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"1ZkQEmh5ANn\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"0KsiqU7uz6ecQ\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Brain\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"nrOMFbiD09E\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"QnKfskbAhz0\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" Fl\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"q6Uz9SRPWbTCf\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"ZFMpp0BC39uzCN\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" reasoning\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"2MwmX9\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" stream\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"2aJP5sGuM\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" instrumentation\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"obfuscation\":\"UvySj7uLIjgfiB\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"lookupId\\\":\\\"flue-session-2026\\\",\\\"query\\\":\\\"Braintrust Flue reasoning stream instrumentation\\\"}\",\"item_id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"output_index\":0,\"sequence_number\":24}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"lookupId\\\":\\\"flue-session-2026\\\",\\\"query\\\":\\\"Braintrust Flue reasoning stream instrumentation\\\"}\",\"call_id\":\"call_fGesVCba2aABMoxSNfVnDVnR\",\"name\":\"web_search\"},\"output_index\":0,\"sequence_number\":25}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_045458052dc10379016a705b6070c48191b73ad8a8f9b550f7\",\"object\":\"response\",\"created_at\":1785748320,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748320,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_045458052dc10379016a705b60c92c81918da2fab9710ee199\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"lookupId\\\":\\\"flue-session-2026\\\",\\\"query\\\":\\\"Braintrust Flue reasoning stream instrumentation\\\"}\",\"call_id\":\"call_fGesVCba2aABMoxSNfVnDVnR\",\"name\":\"web_search\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1055,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":34,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1089},\"user\":null,\"metadata\":{}},\"sequence_number\":26}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432ba7c1a7bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:00 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "236", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998488", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_b7babd52eb11438ca65da8e3e06cd733" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 2, - "id": "5d2312f425df9f2f", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:02.466Z", - "request": { - "body": { - "kind": "json", - "value": { - "include": ["reasoning.encrypted_content"], - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"query\":\"flue instrumentation\"}", - "call_id": "call_rBgonKxWHwhN9E8aQ4VobIOe", - "id": "fc_045458052dc10379016a705b6008408191b27653755cf5bd03", - "name": "lookup", - "type": "function_call" - }, - { - "call_id": "call_rBgonKxWHwhN9E8aQ4VobIOe", - "output": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "function_call_output" - }, - { - "arguments": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\"}", - "call_id": "call_fGesVCba2aABMoxSNfVnDVnR", - "id": "fc_045458052dc10379016a705b60c92c81918da2fab9710ee199", - "name": "web_search", - "type": "function_call" - }, - { - "call_id": "call_fGesVCba2aABMoxSNfVnDVnR", - "output": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "low", - "summary": "auto" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Return a deterministic lookup result with an id needed by web_search.", - "name": "lookup", - "parameters": { - "properties": { - "query": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search a deterministic local web index. Requires the id returned by lookup.", - "name": "web_search", - "parameters": { - "properties": { - "lookupId": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "required": ["lookupId", "query"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Summarize the selected deterministic source after web_search returns a URL.", - "name": "summarize_source", - "parameters": { - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_045458052dc10379016a705b611e8881919ad2adde8d6874b9\",\"object\":\"response\",\"created_at\":1785748321,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_045458052dc10379016a705b611e8881919ad2adde8d6874b9\",\"object\":\"response\",\"created_at\":1785748321,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_4oiajYhDnktEd6wZwQjSMwpb\",\"name\":\"summarize_source\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"hK03W2coG6GFdN\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"url\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"Uu8U6Kh8Nj5Gs\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"sI2ly4ADW4764\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"https\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"ub8xQfxuyrk\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"://\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"JvDy4VPF3RT8t\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"example\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"nUx1jB9C1\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".test\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"nESgdh4F5D9\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"3SOSypdEqxfoT\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"WfNnPvGWaIcJYU\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/re\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"GSIAZIvM9SbTS\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ason\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"ELHGgjctvQHZ\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ing\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"AGLq1WDbPPiDU\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-stream\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"BuKw2YnJj\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"s\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"szEOAd66hgyQxiR\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"obfuscation\":\"nWuCGD2j8eK0lG\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"url\\\":\\\"https://example.test/flue/reasoning-streams\\\"}\",\"item_id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"output_index\":0,\"sequence_number\":18}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"url\\\":\\\"https://example.test/flue/reasoning-streams\\\"}\",\"call_id\":\"call_4oiajYhDnktEd6wZwQjSMwpb\",\"name\":\"summarize_source\"},\"output_index\":0,\"sequence_number\":19}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_045458052dc10379016a705b611e8881919ad2adde8d6874b9\",\"object\":\"response\",\"created_at\":1785748321,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748321,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_045458052dc10379016a705b6194148191a2820dc720d327c5\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"url\\\":\\\"https://example.test/flue/reasoning-streams\\\"}\",\"call_id\":\"call_4oiajYhDnktEd6wZwQjSMwpb\",\"name\":\"summarize_source\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1147,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":30,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1177},\"user\":null,\"metadata\":{}},\"sequence_number\":20}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432be8c937bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:01 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "326", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998398", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_65060d10d7dc405b99f44cac4ba179c7" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 3, - "id": "83cbbf6da482c905", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:03.092Z", - "request": { - "body": { - "kind": "json", - "value": { - "include": ["reasoning.encrypted_content"], - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"query\":\"flue instrumentation\"}", - "call_id": "call_rBgonKxWHwhN9E8aQ4VobIOe", - "id": "fc_045458052dc10379016a705b6008408191b27653755cf5bd03", - "name": "lookup", - "type": "function_call" - }, - { - "call_id": "call_rBgonKxWHwhN9E8aQ4VobIOe", - "output": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "function_call_output" - }, - { - "arguments": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\"}", - "call_id": "call_fGesVCba2aABMoxSNfVnDVnR", - "id": "fc_045458052dc10379016a705b60c92c81918da2fab9710ee199", - "name": "web_search", - "type": "function_call" - }, - { - "call_id": "call_fGesVCba2aABMoxSNfVnDVnR", - "output": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "function_call_output" - }, - { - "arguments": "{\"url\":\"https://example.test/flue/reasoning-streams\"}", - "call_id": "call_4oiajYhDnktEd6wZwQjSMwpb", - "id": "fc_045458052dc10379016a705b6194148191a2820dc720d327c5", - "name": "summarize_source", - "type": "function_call" - }, - { - "call_id": "call_4oiajYhDnktEd6wZwQjSMwpb", - "output": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "low", - "summary": "auto" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Return a deterministic lookup result with an id needed by web_search.", - "name": "lookup", - "parameters": { - "properties": { - "query": { - "type": "string" - } - }, - "required": ["query"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search a deterministic local web index. Requires the id returned by lookup.", - "name": "web_search", - "parameters": { - "properties": { - "lookupId": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "required": ["lookupId", "query"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Summarize the selected deterministic source after web_search returns a URL.", - "name": "summarize_source", - "parameters": { - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_045458052dc10379016a705b6286dc8191be79bd3493b23c38\",\"object\":\"response\",\"created_at\":1785748322,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_045458052dc10379016a705b6286dc8191be79bd3493b23c38\",\"object\":\"response\",\"created_at\":1785748322,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"PROM\",\"item_id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"logprobs\":[],\"obfuscation\":\"ttkbAlDp50d6\",\"output_index\":0,\"sequence_number\":4}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"PT\",\"item_id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"logprobs\":[],\"obfuscation\":\"Z6PXaim62hCyKh\",\"output_index\":0,\"sequence_number\":5}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"_DONE\",\"item_id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"logprobs\":[],\"obfuscation\":\"MiAXFIn3IuM\",\"output_index\":0,\"sequence_number\":6}", - "event: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":7,\"text\":\"PROMPT_DONE\"}", - "event: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"PROMPT_DONE\"},\"sequence_number\":8}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"PROMPT_DONE\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":9}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_045458052dc10379016a705b6286dc8191be79bd3493b23c38\",\"object\":\"response\",\"created_at\":1785748322,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748323,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"PROMPT_DONE\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ma\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Return a deterministic lookup result with an id needed by web_search.\",\"name\":\"lookup\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search a deterministic local web index. Requires the id returned by lookup.\",\"name\":\"web_search\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"lookupId\",\"query\"],\"properties\":{\"lookupId\":{\"type\":\"string\"},\"query\":{\"type\":\"string\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Summarize the selected deterministic source after web_search returns a URL.\",\"name\":\"summarize_source\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1222,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":7,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1229},\"user\":null,\"metadata\":{}},\"sequence_number\":10}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432c7682f7bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:02 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "281", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998323", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_89fa79409a904f43a453a36d2ca890d0" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 4, - "id": "417e64addcf59da7", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:04.374Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "input_text" - } - ], - "role": "user" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0f099b3fce0721fd016a705b63235881a393625d6974243e44\",\"object\":\"response\",\"created_at\":1785748323,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::sk\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0f099b3fce0721fd016a705b63235881a393625d6974243e44\",\"object\":\"response\",\"created_at\":1785748323,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::sk\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_R2a5LOml0NZqq5XTcTCKfbMW\",\"name\":\"task\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"CbHdtJmEy68R82\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"description\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"N0n5J\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"oVS9emhElTbAe\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Run\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"44SOZV25Sx36v\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" the\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"5rEAHjD7s9AY\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" deterministic\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"XS\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" Fl\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"M9xvWA7f348PV\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"Q6wOJG5PkJoZVd\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" instrumentation\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" e\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"YPWsSbID38RZ8M\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"DoKfuME0Em0z2CG\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"XBFbIY4Fow2p3wz\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" test\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"JX98m2RkrWD\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" skill\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"yWTHn4IL3k\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" e\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"tJChmRwpGkHi7f\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"59X6X9YSOr0tsbm\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"YX8fGT8PwVkyRUz\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-fl\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"2vD42kigSA1dk\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"SR8lnZvAOFNlAd\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-s\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"e5LCrEk4SxTUxd\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"kill\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"yQv5KXABVNyh\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".\\\",\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"3SJXRzWCr6dy\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"prompt\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"jnnoMIl9VV\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"cbOKoZJQXGHZv\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Run\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"lOwODhO09Mw2T\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" the\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"HJKMWHVzo3e8\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" skill\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"pfSw7BP6rF\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" named\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"gdjMLdp6nl\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \\\\\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"hb129i9TWIbbM\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"EUDmWBk8oSvNHEU\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"PkeI32czJqwmlmr\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"H2UQSRf8sMZNRBG\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-fl\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"coZw5rkSM9llJ\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"RTDrspHm2QXfY9\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-s\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"dhLU5GXYLswOhi\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"kill\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"PNAPCNmx4QJp\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\\\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"CpbMQHjxstrcsu\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" with\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"KOoqv80ko3G\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" arguments\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"jcYH3O\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" {\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"szXf5lZHyCpilh\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\\\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"52sGZR8YVknFj3\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"marker\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"J8ZugJCeBJ\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\\\\\":\\\\\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"2Ffk7q5BGoV\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"SK\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"xeKflxURNDoVyb\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ILL\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"XjKJ0bhJcewlH\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"_DONE\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"R4FhmYdoiCO\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\\\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"X6XdGVk1CC6NjY\",\"output_index\":0,\"sequence_number\":49}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}.\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"YYRBPkT64KLrlE\",\"output_index\":0,\"sequence_number\":50}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" Output\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"tzFy24CSr\",\"output_index\":0,\"sequence_number\":51}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" only\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"rf2kD3jJIfw\",\"output_index\":0,\"sequence_number\":52}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" the\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"MZEAVTJ1ktEY\",\"output_index\":0,\"sequence_number\":53}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" marker\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"hsFGNt1CH\",\"output_index\":0,\"sequence_number\":54}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" and\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"7jo0OqDghkTU\",\"output_index\":0,\"sequence_number\":55}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" no\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"x7Q4KF2sTB6xe\",\"output_index\":0,\"sequence_number\":56}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" extra\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"5lWyL7VcaY\",\"output_index\":0,\"sequence_number\":57}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" text\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"P6M5oge56Cp\",\"output_index\":0,\"sequence_number\":58}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" when\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"aSZOArmUWCv\",\"output_index\":0,\"sequence_number\":59}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" finished\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"Bcwcv1j\",\"output_index\":0,\"sequence_number\":60}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".\\\",\\\"\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"QPfZWJIaOZZK\",\"output_index\":0,\"sequence_number\":61}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cwd\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"8CNMuVP6JkXXp\",\"output_index\":0,\"sequence_number\":62}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"Zyun5ONtg0VES4\",\"output_index\":0,\"sequence_number\":63}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"/\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"0hQ1Tpvy40v6ov\",\"output_index\":0,\"sequence_number\":64}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"AX7KPkq2wczcX\",\"output_index\":0,\"sequence_number\":65}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"gqTRHNnYCqD98\",\"output_index\":0,\"sequence_number\":66}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"gkSS5rmctsBm7v\",\"output_index\":0,\"sequence_number\":67}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"Mfu7ocBSyP\",\"output_index\":0,\"sequence_number\":68}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"RYfnUiUEM7nSWVO\",\"output_index\":0,\"sequence_number\":69}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"9TJqEA9kGKQ\",\"output_index\":0,\"sequence_number\":70}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"lYlMUrHuRU3\",\"output_index\":0,\"sequence_number\":71}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"7rcyz6emzCww\",\"output_index\":0,\"sequence_number\":72}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"iuArNS0owWYS7ks\",\"output_index\":0,\"sequence_number\":73}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"aO4xFHIWApy\",\"output_index\":0,\"sequence_number\":74}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"MJOzy2JsM8I\",\"output_index\":0,\"sequence_number\":75}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"sO3CIxyDjZko\",\"output_index\":0,\"sequence_number\":76}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"NXKXqePfpHdojz\",\"output_index\":0,\"sequence_number\":77}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"r2Ux15z\",\"output_index\":0,\"sequence_number\":78}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"Kab8AO35yKTKmS\",\"output_index\":0,\"sequence_number\":79}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"tIH5zoiqj9CTjyo\",\"output_index\":0,\"sequence_number\":80}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"iLppUWbjIW0Alm6\",\"output_index\":0,\"sequence_number\":81}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/.\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"eBh1cQtXPmFWjX\",\"output_index\":0,\"sequence_number\":82}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"bt\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"ejJv2wp49FgCgw\",\"output_index\":0,\"sequence_number\":83}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-t\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"Pe5APONBPL5Cz5\",\"output_index\":0,\"sequence_number\":84}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"mp\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"8L98Ly29NFwgR8\",\"output_index\":0,\"sequence_number\":85}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/run\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"zhXouMXCcpBH\",\"output_index\":0,\"sequence_number\":86}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"tfHJ3cnyfuchLJx\",\"output_index\":0,\"sequence_number\":87}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"9\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"Dj1UH61VeDE9CVJ\",\"output_index\":0,\"sequence_number\":88}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"sp\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"mEcW9oStLYq57Z\",\"output_index\":0,\"sequence_number\":89}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"u\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"da4hpEx6UsGmIdX\",\"output_index\":0,\"sequence_number\":90}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Hg\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"wZYe5LO1qqwywa\",\"output_index\":0,\"sequence_number\":91}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"FPrjbypLHyxRn\",\"output_index\":0,\"sequence_number\":92}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"pj0NPDTch\",\"output_index\":0,\"sequence_number\":93}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"xd0ux5YOhgezS\",\"output_index\":0,\"sequence_number\":94}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"cETdOP2IPG6g1d\",\"output_index\":0,\"sequence_number\":95}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"MzOzX2St8G3JA\",\"output_index\":0,\"sequence_number\":96}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"TbtAamDa\",\"output_index\":0,\"sequence_number\":97}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"CJ8PGzQrtmy\",\"output_index\":0,\"sequence_number\":98}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"obfuscation\":\"wrvfq006IzH0mA\",\"output_index\":0,\"sequence_number\":99}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"description\\\":\\\"Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.\\\",\\\"prompt\\\":\\\"Run the skill named \\\\\\\"e2e-flue-skill\\\\\\\" with arguments {\\\\\\\"marker\\\\\\\":\\\\\\\"SKILL_DONE\\\\\\\"}. Output only the marker and no extra text when finished.\\\",\\\"cwd\\\":\\\".\\\"}\",\"item_id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"output_index\":0,\"sequence_number\":100}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"description\\\":\\\"Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.\\\",\\\"prompt\\\":\\\"Run the skill named \\\\\\\"e2e-flue-skill\\\\\\\" with arguments {\\\\\\\"marker\\\\\\\":\\\\\\\"SKILL_DONE\\\\\\\"}. Output only the marker and no extra text when finished.\\\",\\\"cwd\\\":\\\".\\\"}\",\"call_id\":\"call_R2a5LOml0NZqq5XTcTCKfbMW\",\"name\":\"task\"},\"output_index\":0,\"sequence_number\":101}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0f099b3fce0721fd016a705b63235881a393625d6974243e44\",\"object\":\"response\",\"created_at\":1785748323,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748324,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"description\\\":\\\"Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.\\\",\\\"prompt\\\":\\\"Run the skill named \\\\\\\"e2e-flue-skill\\\\\\\" with arguments {\\\\\\\"marker\\\\\\\":\\\\\\\"SKILL_DONE\\\\\\\"}. Output only the marker and no extra text when finished.\\\",\\\"cwd\\\":\\\".\\\"}\",\"call_id\":\"call_R2a5LOml0NZqq5XTcTCKfbMW\",\"name\":\"task\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::sk\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":847,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":109,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":956},\"user\":null,\"metadata\":{}},\"sequence_number\":102}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432cb5fc97bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:03 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "206", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998698", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_4f8a543160de4adea11337a977a14713" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 5, - "id": "341675a0a1a88c63", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:05.801Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b648f7081a3b96733816812acb8\",\"object\":\"response\",\"created_at\":1785748324,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b648f7081a3b96733816812acb8\",\"object\":\"response\",\"created_at\":1785748324,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_w6pyYZgnhlSOAXul75DAG1Cj\",\"name\":\"task\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"ljpLbllVFarTqw\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"description\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"dI5yK\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"gaE80vRkyue6q\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Run\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"1yh2B3GwgUIkU\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"Aoc2XDyvRflwpK\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"gKDO3Feq1OjvKMT\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"d2O2IlHGALtgxiz\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"c9m5GXjLlVm1X\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"NUY5MBuNzz8sZQ\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"9Pwv64hhPdkCsg\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"kill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"iOoKz15nYfXp\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" with\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"UCahcEprOGt\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" marker\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"TaOkeLqZF\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" SK\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"VxlrD2puv3ABI\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ILL\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"DebTGrlabFRQY\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"_DONE\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"Y6K3aF28EHz\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"py2Pd0lnhbcOC\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"prompt\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"PM2ySL1u5c\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"GEFeUIpfX9F6O\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Run\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"g8vFm9KNQk3gN\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" the\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"cXxSzWyVKRHs\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" skill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"bwbVemN5vW\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" named\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"4DdymViVIi\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \\\\\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"9QwzJKU8yuXI1\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"qc8c9FjKJwS85NJ\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"ewNZxFO4TdAgDUM\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"Y8xyYUu8EtAbcT1\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"88SoGMEyRHz8M\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"DpP2H8WkpDUzag\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"5jctq9RddqcIt8\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"kill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"xpmcaPHyYGYa\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\\\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"kumygUHU0Bd682\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" with\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"1aD8huNzRAg\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" arguments\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"u62pY1\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" {\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"DlufXJUKJUw7nw\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\\\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"dqIkwjYJ3Ruj51\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"marker\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"stUcL5JVqc\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\\\\\":\\\\\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"cUfPJq2cus1\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"SK\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"ZzPWTx2aFPwBYM\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ILL\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"Hbw389fTXAJOT\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"_DONE\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"ALgrsM7YeM5\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\\\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"D4fcykCofi3ap6\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}.\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"79o07UPm9B29cw\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" Ensure\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"1JXF9XGzd\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" completion\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"vfueK\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" and\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"VlKAzFGFtrcw\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" report\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"wBeH1rhXr\",\"output_index\":0,\"sequence_number\":49}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" the\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"6EO4vypCMr7q\",\"output_index\":0,\"sequence_number\":50}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" final\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"0pF35NFSxj\",\"output_index\":0,\"sequence_number\":51}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" output\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"Fyh51oBFK\",\"output_index\":0,\"sequence_number\":52}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" marker\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"2fdE8CEMT\",\"output_index\":0,\"sequence_number\":53}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" only\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"Oi1kYTafLLr\",\"output_index\":0,\"sequence_number\":54}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"JHNARwrOOg28\",\"output_index\":0,\"sequence_number\":55}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"agent\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"JujpXurFNFJ\",\"output_index\":0,\"sequence_number\":56}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"7UtnbpjQ9Vd5r\",\"output_index\":0,\"sequence_number\":57}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"lUMgv4pWgFguMgN\",\"output_index\":0,\"sequence_number\":58}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"lquqyIOhUG8FFqb\",\"output_index\":0,\"sequence_number\":59}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"z1aAXV10WTtEwfO\",\"output_index\":0,\"sequence_number\":60}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"z29NnUuO8awvU\",\"output_index\":0,\"sequence_number\":61}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"vjUb5xZd4tz6Nc\",\"output_index\":0,\"sequence_number\":62}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"S79uypIplW7mcH\",\"output_index\":0,\"sequence_number\":63}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"kill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"GgbDCJps9iY5\",\"output_index\":0,\"sequence_number\":64}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"vCCHCimmRyAgx\",\"output_index\":0,\"sequence_number\":65}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cwd\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"AnLDQiNay8ENp\",\"output_index\":0,\"sequence_number\":66}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"6YVXzJiSMPKoqe\",\"output_index\":0,\"sequence_number\":67}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"2iVYgZDItlXYju\",\"output_index\":0,\"sequence_number\":68}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"p8iffYf14P4rP\",\"output_index\":0,\"sequence_number\":69}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"jHIrIgM5myBm6\",\"output_index\":0,\"sequence_number\":70}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"Yud9IuaLw1CQTi\",\"output_index\":0,\"sequence_number\":71}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"sxVNzIOgmI\",\"output_index\":0,\"sequence_number\":72}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"BFftBcl4q4mRiic\",\"output_index\":0,\"sequence_number\":73}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"bvKWRZsGF3z\",\"output_index\":0,\"sequence_number\":74}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"8fCBYfWjld6\",\"output_index\":0,\"sequence_number\":75}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"bWCV0wB1yOIj\",\"output_index\":0,\"sequence_number\":76}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"gMLQ5nGNZU5RQcA\",\"output_index\":0,\"sequence_number\":77}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"HbDV4gJR67J\",\"output_index\":0,\"sequence_number\":78}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"fY6Fye7lRfU\",\"output_index\":0,\"sequence_number\":79}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"nDcCSPqarvNr\",\"output_index\":0,\"sequence_number\":80}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"CDVkHgmi29Aam7\",\"output_index\":0,\"sequence_number\":81}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"yMH6cPx\",\"output_index\":0,\"sequence_number\":82}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"zoEfWKIxJ3vi4r\",\"output_index\":0,\"sequence_number\":83}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"ti33GAqnm3z55of\",\"output_index\":0,\"sequence_number\":84}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"iKabCz0geb00km7\",\"output_index\":0,\"sequence_number\":85}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/.\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"kQaYnx9LpaZ4YA\",\"output_index\":0,\"sequence_number\":86}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"bt\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"rHzMr4AWuPUfim\",\"output_index\":0,\"sequence_number\":87}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-t\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"nJma8KAPeFozgD\",\"output_index\":0,\"sequence_number\":88}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"mp\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"bJV57iCDmqv2VA\",\"output_index\":0,\"sequence_number\":89}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/run\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"IpVQRAjXMPE4\",\"output_index\":0,\"sequence_number\":90}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"pzJyXElP7ou0ovn\",\"output_index\":0,\"sequence_number\":91}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"9\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"1ZlIYGuEL0CGzns\",\"output_index\":0,\"sequence_number\":92}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"sp\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"QVS1D8jJbqlZ7X\",\"output_index\":0,\"sequence_number\":93}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"u\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"2mhaJD9do70hkwJ\",\"output_index\":0,\"sequence_number\":94}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Hg\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"dXooQVlSHUeRZP\",\"output_index\":0,\"sequence_number\":95}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"JYgibM5tWMAjH\",\"output_index\":0,\"sequence_number\":96}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"JH3lHc5hu\",\"output_index\":0,\"sequence_number\":97}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"S7XC3UsMmTfuf\",\"output_index\":0,\"sequence_number\":98}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"xWgqKM1XAI7Gka\",\"output_index\":0,\"sequence_number\":99}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"7l79NEJnKe3pM\",\"output_index\":0,\"sequence_number\":100}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"F9vfZGnW\",\"output_index\":0,\"sequence_number\":101}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"iNKeVMoTNBN\",\"output_index\":0,\"sequence_number\":102}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"obfuscation\":\"dJaOqvNAHb8EH5\",\"output_index\":0,\"sequence_number\":103}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"description\\\":\\\"Run e2e-flue-skill with marker SKILL_DONE\\\",\\\"prompt\\\":\\\"Run the skill named \\\\\\\"e2e-flue-skill\\\\\\\" with arguments {\\\\\\\"marker\\\\\\\":\\\\\\\"SKILL_DONE\\\\\\\"}. Ensure completion and report the final output marker only.\\\",\\\"agent\\\":\\\"e2e-flue-skill\\\",\\\"cwd\\\":\\\".\\\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"output_index\":0,\"sequence_number\":104}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"description\\\":\\\"Run e2e-flue-skill with marker SKILL_DONE\\\",\\\"prompt\\\":\\\"Run the skill named \\\\\\\"e2e-flue-skill\\\\\\\" with arguments {\\\\\\\"marker\\\\\\\":\\\\\\\"SKILL_DONE\\\\\\\"}. Ensure completion and report the final output marker only.\\\",\\\"agent\\\":\\\"e2e-flue-skill\\\",\\\"cwd\\\":\\\".\\\"}\",\"call_id\":\"call_w6pyYZgnhlSOAXul75DAG1Cj\",\"name\":\"task\"},\"output_index\":0,\"sequence_number\":105}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b648f7081a3b96733816812acb8\",\"object\":\"response\",\"created_at\":1785748324,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748325,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"description\\\":\\\"Run e2e-flue-skill with marker SKILL_DONE\\\",\\\"prompt\\\":\\\"Run the skill named \\\\\\\"e2e-flue-skill\\\\\\\" with arguments {\\\\\\\"marker\\\\\\\":\\\\\\\"SKILL_DONE\\\\\\\"}. Ensure completion and report the final output marker only.\\\",\\\"agent\\\":\\\"e2e-flue-skill\\\",\\\"cwd\\\":\\\".\\\"}\",\"call_id\":\"call_w6pyYZgnhlSOAXul75DAG1Cj\",\"name\":\"task\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":855,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":113,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":968},\"user\":null,\"metadata\":{}},\"sequence_number\":106}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432d37fd37bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:04 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "268", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998692", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_bf6f5e02b3da40c19ef5905452f8e5f6" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 6, - "id": "cbb9070d68f84d1b", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:06.856Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b65dc0881a3a79d930ad7069d95\",\"object\":\"response\",\"created_at\":1785748325,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b65dc0881a3a79d930ad7069d95\",\"object\":\"response\",\"created_at\":1785748325,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_6zEBnJRmXFIkGFwNjWLZP4uC\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"qYr2RE37LRYj8T\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"command\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"5olycNCp4\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"TtIA15U6WTwyB\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ls\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"8gpmFqa6OBiMdp\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"HqbfEOyA6n1XM5\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"la\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"zkZ09ufXuvTuYH\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"IsgyMPXjN3RyQT\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"QwfPCO1ymGdY7\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"ktEAC1neLBJD0\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"qSxXdSs4TsmFt5\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"NIrFRZbher\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"h1HKJqOxjX7neu9\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"g5de6pXeQeu\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"O8W7X6T6QJ9\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"ZwdOUP0nNaaU\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"RdDtnAGnoUWZHMl\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"bJadnE3z1Pn\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"QdtHJab2Nju\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"d0BalsPUN30E\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"zkZSKvyjjnJqmd\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"gidR8ss\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"sTij2BeE8hhf4F\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"U6tRlf8eVA0ZfFt\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"3iym56BtyHXWWzj\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/.\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"yPazRePg1CYCjy\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"bt\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"XlYRyV5eP5RuAi\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-t\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"cEUjV7aq8IJmrd\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"mp\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"llpLkANV2zyfnb\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/run\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"flFQ8Wd13kY2\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"6NV0XhXU74sXecB\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"9\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"3e8DIPv3Q4zimTJ\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"sp\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"CcYFgrShio2OGa\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"u\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"SB8CftjcqKYqDAq\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Hg\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"o0TbOyqaOwnbjc\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"AxgExC2R2UrOj\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"GktP5dj47\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"D8Qyuf0OZ5udR\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"rTVImQr26P3Vsq\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"SPXiRAIfqk20q\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"ukKgXlCi\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"q5sV1H9oEPR\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/.\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"Vbdq3gYacR4ydn\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"agents\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"iYjX4AJgNl\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" &&\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"QpXEqJPwcIzx8\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" find\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"idAm4VrdUd9\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"xlH3WH5e7K7TVT\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"ci7N2JhvoR4v3\",\"output_index\":0,\"sequence_number\":49}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"lNESXh61G4kcn\",\"output_index\":0,\"sequence_number\":50}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"JHLNRMdy9opWYh\",\"output_index\":0,\"sequence_number\":51}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"dgKik8Wq8j\",\"output_index\":0,\"sequence_number\":52}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"ivSoP1Az4lunbMK\",\"output_index\":0,\"sequence_number\":53}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"ei4Les3EoOx\",\"output_index\":0,\"sequence_number\":54}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"j583CvShsMn\",\"output_index\":0,\"sequence_number\":55}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"8pCfiAeFxnGs\",\"output_index\":0,\"sequence_number\":56}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"BrQSS7SwzKOQJMI\",\"output_index\":0,\"sequence_number\":57}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"mq3EFmYB311\",\"output_index\":0,\"sequence_number\":58}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"hO6jOpGPatJ\",\"output_index\":0,\"sequence_number\":59}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"lbKGYWB3E1BM\",\"output_index\":0,\"sequence_number\":60}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"u16CBhAKfJTH5L\",\"output_index\":0,\"sequence_number\":61}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"5hJroXc\",\"output_index\":0,\"sequence_number\":62}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"33pMuKaTNDmMqZ\",\"output_index\":0,\"sequence_number\":63}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"7PfD7iSXELGdvpH\",\"output_index\":0,\"sequence_number\":64}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"l89ZUwAGJ4COOHT\",\"output_index\":0,\"sequence_number\":65}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"heNzYKqh1OCQTD\",\"output_index\":0,\"sequence_number\":66}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"max\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"sU1Q9AhPAADW3\",\"output_index\":0,\"sequence_number\":67}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"depth\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"G2KErWebcGP\",\"output_index\":0,\"sequence_number\":68}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"Hk3AzR5adCxbnFT\",\"output_index\":0,\"sequence_number\":69}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"4\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"2AwssIctaSpQjDt\",\"output_index\":0,\"sequence_number\":70}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"qTL2wxyksScrpT\",\"output_index\":0,\"sequence_number\":71}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"name\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"9dAiG7pNttSR\",\"output_index\":0,\"sequence_number\":72}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" AG\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"vyWvpkSniB10E\",\"output_index\":0,\"sequence_number\":73}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ENTS\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"8X34qqxb7k4T\",\"output_index\":0,\"sequence_number\":74}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".md\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"srlvyGZKNg8fy\",\"output_index\":0,\"sequence_number\":75}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"kX3C3ee4M4LAQZ\",\"output_index\":0,\"sequence_number\":76}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"print\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"kuuWd4dbfED\",\"output_index\":0,\"sequence_number\":77}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"t1AZzeTRFs1dW\",\"output_index\":0,\"sequence_number\":78}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"timeout\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"J9nNvn2yP\",\"output_index\":0,\"sequence_number\":79}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"HoGIKlERH8SubV\",\"output_index\":0,\"sequence_number\":80}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"100\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"K0vUuFSWy0H29\",\"output_index\":0,\"sequence_number\":81}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"000\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"iki9PdBtWpJFT\",\"output_index\":0,\"sequence_number\":82}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"obfuscation\":\"XUhEnBL2ma1rfYy\",\"output_index\":0,\"sequence_number\":83}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 4 -name AGENTS.md -print\\\",\\\"timeout\\\":100000}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"output_index\":0,\"sequence_number\":84}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 4 -name AGENTS.md -print\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_6zEBnJRmXFIkGFwNjWLZP4uC\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":85}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b65dc0881a3a79d930ad7069d95\",\"object\":\"response\",\"created_at\":1785748325,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748326,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 4 -name AGENTS.md -print\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_6zEBnJRmXFIkGFwNjWLZP4uC\",\"name\":\"bash\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1002,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":94,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1096},\"user\":null,\"metadata\":{}},\"sequence_number\":86}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432dc5a4c7bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:06 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "201", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998545", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_3ec73f40ef984f3ea2f7b4df76b2890e" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 7, - "id": "bccc54d212e2503d", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:08.056Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -name AGENTS.md -print\",\"timeout\":100000}", - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "id": "fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "output": "total 4\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 09:10 .\ndrwxr-xr-x 10 vercel-sandbox vercel-sandbox 4096 Aug 3 09:11 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 09:10 skills", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b66ec3081a3b53487cd28a912dd\",\"object\":\"response\",\"created_at\":1785748326,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b66ec3081a3b53487cd28a912dd\",\"object\":\"response\",\"created_at\":1785748326,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_jQUCie0ZgaXUrZCDcezp0msl\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"5s5fPUr1lnx6LN\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"command\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"EzIXsl5P7\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"rfpcd360vn0Zc\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"find\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"gbSugqPh322J\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"un8JuSPgzNW7Tq\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"AT24QvJgyIKCb\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"M2WqPhiDm0W0B\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"HnCl5JHASkkvRV\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"9hqImMoIYc\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"G2MFkp35u1CeVM3\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"S9DNif6AAaj\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"KFRgE1qwn8O\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"iRZhcqXd9XRs\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"9XF5P57pZo3uNAy\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"4P9IWtruXv8\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"6tLj1KQL0OS\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"ZJX7xdMeL6wZ\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"LJ7CGVyndyKtYf\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"xJcnaku\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"PfTOXv6id59uZk\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"ZygymPXfo8RsWR7\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"tcZjEPZR3q54QCO\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/.\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"Is3Ft8kklGHlSC\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"bt\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"J9UUPkfaryOCWA\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-t\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"tUpNGsjI3owh2O\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"mp\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"uhvu4t3Z33cBrQ\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/run\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"EY7RAwkNVqHj\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"WzBJvCmbkvTjzRe\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"9\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"T1c8ikRVDyXs3K4\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"sp\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"TLa4CtuK49G4fL\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"u\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"vUKKjMsXw5sIkHv\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Hg\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"SD4uFMMoVX66s1\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"dsOEaf4EkLy14\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"Sk1iL5lxc\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"j3F97pquP3wT0\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"5SHqC6FW1engeD\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"MKvWbeQhFJuyU\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"0JPR573Y\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"tJ0iHGcFnz2\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"2Xe2zCjtFpD1Aa\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"max\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"9fKJvO4eySsc7\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"depth\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"adTuj16Pog1\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"RG1GXpp0dtqROaL\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"4\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"62nieO7lhOujIMV\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"f85mHnnTvDNVsm\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"type\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"oOkDwdNirXZn\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" f\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"7hsgm1tQLQmfWU\",\"output_index\":0,\"sequence_number\":49}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"TEtJIF7zjvh5BC\",\"output_index\":0,\"sequence_number\":50}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"name\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"TRWuvVnlxjuw\",\"output_index\":0,\"sequence_number\":51}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" AG\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"OPMH46rRaY9wj\",\"output_index\":0,\"sequence_number\":52}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ENTS\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"Wx1ktH8noul5\",\"output_index\":0,\"sequence_number\":53}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".md\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"Oo8KevjbHLtSH\",\"output_index\":0,\"sequence_number\":54}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"W1tKVt07sDFz6L\",\"output_index\":0,\"sequence_number\":55}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"print\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"Ca0wTzfowmG\",\"output_index\":0,\"sequence_number\":56}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"InAeZlZgCo2EWg\",\"output_index\":0,\"sequence_number\":57}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"exec\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"xuVbI7qC8ZR7\",\"output_index\":0,\"sequence_number\":58}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" sed\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"rWSvxjZ31UQg\",\"output_index\":0,\"sequence_number\":59}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"LsdCqDwIUZ1aXY\",\"output_index\":0,\"sequence_number\":60}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"n\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"9t5AteQjutiyqe3\",\"output_index\":0,\"sequence_number\":61}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" '\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"b8zSZflAk9EXLf\",\"output_index\":0,\"sequence_number\":62}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"1\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"i7Uvk2jgzfKhd4j\",\"output_index\":0,\"sequence_number\":63}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\",\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"dY2J1TUAF8vRFo8\",\"output_index\":0,\"sequence_number\":64}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"200\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"3t3yMCd2EF2u3\",\"output_index\":0,\"sequence_number\":65}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"p\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"LI717GeLz7d3rCb\",\"output_index\":0,\"sequence_number\":66}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"'\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"aUymdN14yxm7RVU\",\"output_index\":0,\"sequence_number\":67}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" {}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"SsZA4kHNYtJqo\",\"output_index\":0,\"sequence_number\":68}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \\\\\\\\\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"VZIldvECynY3p\",\"output_index\":0,\"sequence_number\":69}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\";\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"LXiCXRoQhZXP5HK\",\"output_index\":0,\"sequence_number\":70}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"lN4m4dNvIh4ID\",\"output_index\":0,\"sequence_number\":71}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"timeout\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"bGxGZD252\",\"output_index\":0,\"sequence_number\":72}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"Zz2e8ECX6n6SVc\",\"output_index\":0,\"sequence_number\":73}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"100\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"Zi6V6iOPLR6s7\",\"output_index\":0,\"sequence_number\":74}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"000\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"rp63i4YuRxltd\",\"output_index\":0,\"sequence_number\":75}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"obfuscation\":\"74rLY4yKHw7IjTn\",\"output_index\":0,\"sequence_number\":76}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\\\\\;\\\",\\\"timeout\\\":100000}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"output_index\":0,\"sequence_number\":77}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\\\\\;\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_jQUCie0ZgaXUrZCDcezp0msl\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":78}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b66ec3081a3b53487cd28a912dd\",\"object\":\"response\",\"created_at\":1785748326,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748328,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\\\\\;\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_jQUCie0ZgaXUrZCDcezp0msl\",\"name\":\"bash\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1197,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":87,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1284},\"user\":null,\"metadata\":{}},\"sequence_number\":79}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432e308c57bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:07 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "181", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998347", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_5c4fd875329344cfa3d20c5925805429" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 8, - "id": "8eec740c1be01c5c", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:09.317Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -name AGENTS.md -print\",\"timeout\":100000}", - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "id": "fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "output": "total 4\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 09:10 .\ndrwxr-xr-x 10 vercel-sandbox vercel-sandbox 4096 Aug 3 09:11 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 09:10 skills", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\;\",\"timeout\":100000}", - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "id": "fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "output": "(no output)", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6820c481a3b64f6527345dc3ca\",\"object\":\"response\",\"created_at\":1785748328,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6820c481a3b64f6527345dc3ca\",\"object\":\"response\",\"created_at\":1785748328,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_NgMSUb6wIR0GAf9U8LXERtBQ\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"Ecm4AJUnR33nzy\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"command\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"qAhzrPXMU\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"HjoLYpLsOOApn\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"find\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"COx5RXyau1iu\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"7fcdZxijyLBFbj\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"EdhPBWD97UXXj\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"g4JtDEv0GyNkp\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"Yu3ytbv7EfB9L7\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"loDk0mviWC\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"4KyvKX7bONkWif0\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"yowGulOqnpo\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"83qIG2PGqLt\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"ZFbal7RnszIt\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"GVlOdY86Xo86jAw\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"mjcerP27QOt\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"oQnyazxVeT6\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"2CRHfaUuS1Dd\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"3IfHRkUmRfpbWX\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"uJPFidS\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"PAbcespQ5gkoQu\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"By2Tx9QLwPZRKd1\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"jCZWJ7sTG0eCEsh\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"gu7kgzv2IJ1Nyl\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"max\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"ftZoJcWqYeSjP\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"depth\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"pVAaXJ9DEpZ\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"sR3ARyMb8XihmRv\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"6\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"O5oruVniSAltaiT\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"vv0l4nT2EtqHMG\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"type\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"eCQV3vrXZ37f\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" f\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"LD2tChTMhpkm7O\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"dbjx8JTI5wqqpk\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"name\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"CMGMxsC84MqB\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" '\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"IP4ilgz1kRzQlU\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"AG\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"30S1wp0FSdH8j0\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ENTS\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"LbBUDVVtGwst\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".md\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"8piB3U9Zy7jyA\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"'\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"9DRBJbRy6maC4gS\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"XAt6MDPojUNwz3\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"print\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"2sb7d6gE12q\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"78xkqC87iDA2l\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"timeout\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"LZICbxiFz\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"L39ZgCy9F9L7ou\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"100\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"cmBrFOna5ifiL\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"000\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"z5qEzhvJIbUVN\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"obfuscation\":\"S29PMSSd6WVTIYN\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\\\",\\\"timeout\\\":100000}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"output_index\":0,\"sequence_number\":48}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_NgMSUb6wIR0GAf9U8LXERtBQ\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":49}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6820c481a3b64f6527345dc3ca\",\"object\":\"response\",\"created_at\":1785748328,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748329,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_NgMSUb6wIR0GAf9U8LXERtBQ\",\"name\":\"bash\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1298,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":58,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1356},\"user\":null,\"metadata\":{}},\"sequence_number\":50}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432ea7f5d7bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:08 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "428", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998245", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_e00fc12bdba24599905386f92e325433" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 9, - "id": "6945bf18d345ad4a", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:10.503Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -name AGENTS.md -print\",\"timeout\":100000}", - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "id": "fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "output": "total 4\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 09:10 .\ndrwxr-xr-x 10 vercel-sandbox vercel-sandbox 4096 Aug 3 09:11 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 09:10 skills", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\;\",\"timeout\":100000}", - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "id": "fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\",\"timeout\":100000}", - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "id": "fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "output": "(no output)", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b69681081a381f7cdcf2e5f8c38\",\"object\":\"response\",\"created_at\":1785748329,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b69681081a381f7cdcf2e5f8c38\",\"object\":\"response\",\"created_at\":1785748329,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_BXKiQ5KG8c42XPPp94zgcY3H\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"hG7XwE8kTftCFD\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"command\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"SAESyTCDU\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"FdSHAU6VPfEiP\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"find\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"JTitwOdHc00P\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"Z5ba86g8nOxzKr\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"BUYeIHkrw1ca3\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"Jnlz6e6YIi8An\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"EiAxOlmPQ6R9Lk\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"HLwMNKLqXd\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"CyMa169D7RtZmdr\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"eBBwyrijOzo\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"9oZJd40vJbv\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"au0nu8FGh24U\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"mH2gvFHGB1gaA18\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"W1iZL4cRSMq\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"QUUnH5ISGOi\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"eeLPcF9XcrHV\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"lJ7n3mjPmhDNxo\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"Cq8la1K\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"QCMTFUK5pMEVRm\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"BgRGVixu3UbxQDA\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"aqbUU7TCMgXGNus\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"0aahbmWnQc34bI\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"max\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"hLd8UqgGfFZ9D\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"depth\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"GmhwY6EWAHh\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"NRuEQgh0p3eRHWo\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"5\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"VSUdF5XfszF7J7C\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"7bgbWInYXsOzEZ\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"type\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"ZdXd8MYxzBVa\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" f\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"lKHlgmcHv4yQ7n\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"O3v0EDtKmgDYYi\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"name\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"yOsJv2sjpuJT\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" '*\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"bT4zdeKIYUfHc\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"jujglZGbsBwPa7\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"LKKBJiCjnXh4xf\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"*\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"shy3qPjXsnqPzfn\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"skill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"QCXNC1UuV7e\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"*\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"XG8mVOll3Ohqe18\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"'\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"twOToqPAOsKMMVy\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"DZtAspDqcPPg83\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"o\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"1vqyG924TjjeMm1\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"vbMfw1I6yQT071\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"name\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"yEkTfUjLa7T3\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" '*\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"QbcNOxt5ouCjy\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"skill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"fkrT1lk0jv9\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"*\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"9RVtGG8sxWEOjJr\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"'\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"Za4XJQ2Z2IsOIeb\",\"output_index\":0,\"sequence_number\":49}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" |\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"px85R0MdttFruZ\",\"output_index\":0,\"sequence_number\":50}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" head\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"Y88f7Ld1Ora\",\"output_index\":0,\"sequence_number\":51}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"8moCEbjvgTG1n\",\"output_index\":0,\"sequence_number\":52}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"timeout\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"Kmyh4pQvd\",\"output_index\":0,\"sequence_number\":53}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"GrJ0KmP1JEa4oC\",\"output_index\":0,\"sequence_number\":54}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"100\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"tQ7b2AdVP1fwn\",\"output_index\":0,\"sequence_number\":55}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"000\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"OGHfUzdqw3zns\",\"output_index\":0,\"sequence_number\":56}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"obfuscation\":\"4USiuoYvOOTVjiY\",\"output_index\":0,\"sequence_number\":57}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head\\\",\\\"timeout\\\":100000}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"output_index\":0,\"sequence_number\":58}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_BXKiQ5KG8c42XPPp94zgcY3H\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":59}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b69681081a381f7cdcf2e5f8c38\",\"object\":\"response\",\"created_at\":1785748329,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748330,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_BXKiQ5KG8c42XPPp94zgcY3H\",\"name\":\"bash\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1370,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":68,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1438},\"user\":null,\"metadata\":{}},\"sequence_number\":60}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432f26f707bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:09 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "283", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998176", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_f5479925a7844533a4ae27ed832d027b" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 10, - "id": "ab34eb26595033a1", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:12.905Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -name AGENTS.md -print\",\"timeout\":100000}", - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "id": "fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "output": "total 4\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 09:10 .\ndrwxr-xr-x 10 vercel-sandbox vercel-sandbox 4096 Aug 3 09:11 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 09:10 skills", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\;\",\"timeout\":100000}", - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "id": "fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\",\"timeout\":100000}", - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "id": "fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head\",\"timeout\":100000}", - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "id": "fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "output": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6af7d081a396a76851300752a1\",\"object\":\"response\",\"created_at\":1785748331,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6af7d081a396a76851300752a1\",\"object\":\"response\",\"created_at\":1785748331,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_Zmmrs64LFscpAHsIUaIfEcAN\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"K1hVgurfEtUc37\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"command\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"luCO3JGp3\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"FmitdgZUwsXVs\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ls\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"VZpJ9At0woeytk\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"0xukceLNBwYJiK\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"la\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"1s2AnPNungEZlQ\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"Bfcx9jEBxq6eit\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"1rFsvnpXQsSA2\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"WHxut9fzLrXkW\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"BzqeyKc2WZeyRg\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"ii7jPgkIJ8\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"PkykxLdvlfrkHPL\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"12UfuwAycuU\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"3K3OjXe08nc\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"PkaaeYRGj55a\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"UHfCbDbKjO5lYqr\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"MOTWOSsU43Z\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"dmMQk0sQrGh\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"XtEiI6ADgEIV\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"5ovKtom8zqryPR\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"7eUUx0L\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"9QbEfSAXVpmE07\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"BeGJDWOdDgtyZwk\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"Vio65vRQsUs4hgC\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"6P7cD2KzvBQE3\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"RJSR2IM5S\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"uSAAuJhHqx2h6\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"LURBTZTvJSTf7s\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"b3ZtE4ekxYWXs\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"WUoTVOo4\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"n6TvbFOtdSH\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/.\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"1Lt96k7JA9dUeI\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"agents\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"O7qjKmlEms\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"I1E6t9owIg9P1WR\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"skills\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"21ZpKkvrdb\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"zV3b4fO0R5USoJ\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"wtfZeZvShndVNQM\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"8DgrYaTc5tF9qQN\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"WMhoTEOnE6Xro\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"XaFg3AblnqowIQ\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"FJ1AtHWIUTZkuK\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"kill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"K2KXxbd3uIZt\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" &&\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"ek6YUPE8Cp9AE\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" find\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"XM049MlPwBE\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"SCFb56qc5ogbCE\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"nB5JRT4lhiSpZ\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"DozjlarmrsGSB\",\"output_index\":0,\"sequence_number\":49}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"CvaFfLoJcDammS\",\"output_index\":0,\"sequence_number\":50}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"6yYxC29jBg\",\"output_index\":0,\"sequence_number\":51}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"NpUUfvLkXCyoKMW\",\"output_index\":0,\"sequence_number\":52}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"ym9bVn0oRVM\",\"output_index\":0,\"sequence_number\":53}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"BPomQliEf6i\",\"output_index\":0,\"sequence_number\":54}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"0hcPCne66Cg8\",\"output_index\":0,\"sequence_number\":55}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"TfdWF7zbYNainuF\",\"output_index\":0,\"sequence_number\":56}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"eYEUL1yBJZh\",\"output_index\":0,\"sequence_number\":57}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"EfjIxAAX6o0\",\"output_index\":0,\"sequence_number\":58}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"8hFJ6Wya2ZL8\",\"output_index\":0,\"sequence_number\":59}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"BUmLaB9uO7p4JA\",\"output_index\":0,\"sequence_number\":60}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"TZCxxmh\",\"output_index\":0,\"sequence_number\":61}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"Wkjm95wXRvIPMz\",\"output_index\":0,\"sequence_number\":62}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"IWO2TynZ5pN8CwA\",\"output_index\":0,\"sequence_number\":63}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"AYKtiZZuh0gJgYj\",\"output_index\":0,\"sequence_number\":64}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"rcDuq1t53ub50\",\"output_index\":0,\"sequence_number\":65}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"cPtht6oAy\",\"output_index\":0,\"sequence_number\":66}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"tQudFftzB650V\",\"output_index\":0,\"sequence_number\":67}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"3iUp76L3SIYhqb\",\"output_index\":0,\"sequence_number\":68}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"ZXAa0ULdLud1i\",\"output_index\":0,\"sequence_number\":69}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"kzf0iJxr\",\"output_index\":0,\"sequence_number\":70}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"rw7Suy9RUTq\",\"output_index\":0,\"sequence_number\":71}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/.\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"uPuc49BoOe4rwv\",\"output_index\":0,\"sequence_number\":72}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"agents\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"xCcbSL9wCG\",\"output_index\":0,\"sequence_number\":73}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"0kPSap2qJXeYe5c\",\"output_index\":0,\"sequence_number\":74}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"skills\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"07SeHSKAgE\",\"output_index\":0,\"sequence_number\":75}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"g2UQCFv27V3sqv\",\"output_index\":0,\"sequence_number\":76}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"YniEsIzqh6veIWP\",\"output_index\":0,\"sequence_number\":77}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"64iQG5gdZWPuPkQ\",\"output_index\":0,\"sequence_number\":78}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"Kz4PY7cfevVr9\",\"output_index\":0,\"sequence_number\":79}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"dAurB55pjAoLFj\",\"output_index\":0,\"sequence_number\":80}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"o5PWW9PAQB5Kvq\",\"output_index\":0,\"sequence_number\":81}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"kill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"kEfcPgdMqlKI\",\"output_index\":0,\"sequence_number\":82}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"SJWGXR6ryAJ2DE\",\"output_index\":0,\"sequence_number\":83}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"max\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"4QU605ENItN8B\",\"output_index\":0,\"sequence_number\":84}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"depth\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"4XthyjdZM9w\",\"output_index\":0,\"sequence_number\":85}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"WgGDumDYzUufFUI\",\"output_index\":0,\"sequence_number\":86}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"oQ8wQwbVorceatB\",\"output_index\":0,\"sequence_number\":87}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"sxrOiShGGRNzfx\",\"output_index\":0,\"sequence_number\":88}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"type\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"BwSNPMU6iGnd\",\"output_index\":0,\"sequence_number\":89}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" f\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"Rm35RryQU13YdR\",\"output_index\":0,\"sequence_number\":90}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"zJ5ddKoSwCzVU4\",\"output_index\":0,\"sequence_number\":91}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"print\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"h80D5JBABPH\",\"output_index\":0,\"sequence_number\":92}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"rpdDODEBJorbc\",\"output_index\":0,\"sequence_number\":93}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"timeout\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"p4vKlv5m6\",\"output_index\":0,\"sequence_number\":94}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"Zbhx8YnzpAicDP\",\"output_index\":0,\"sequence_number\":95}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"100\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"r4y6kurWe2zky\",\"output_index\":0,\"sequence_number\":96}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"000\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"x0tuUliZpjxkA\",\"output_index\":0,\"sequence_number\":97}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"obfuscation\":\"VTLJl2mMhQOUOyQ\",\"output_index\":0,\"sequence_number\":98}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"command\\\":\\\"find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print\\\",\\\"timeout\\\":100000}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"output_index\":0,\"sequence_number\":99}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_Zmmrs64LFscpAHsIUaIfEcAN\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":100}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6af7d081a396a76851300752a1\",\"object\":\"response\",\"created_at\":1785748331,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748332,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_Zmmrs64LFscpAHsIUaIfEcAN\",\"name\":\"bash\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1513,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":109,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1622},\"user\":null,\"metadata\":{}},\"sequence_number\":101}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25432f9bf6f7bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:11 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "688", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998032", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_2ceebd83455e4b27b79fc2d72fb5dd3c" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 11, - "id": "250e64f864c72337", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:14.154Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -name AGENTS.md -print\",\"timeout\":100000}", - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "id": "fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "output": "total 4\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 09:10 .\ndrwxr-xr-x 10 vercel-sandbox vercel-sandbox 4096 Aug 3 09:11 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 09:10 skills", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\;\",\"timeout\":100000}", - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "id": "fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\",\"timeout\":100000}", - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "id": "fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head\",\"timeout\":100000}", - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "id": "fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "output": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print\",\"timeout\":100000}", - "call_id": "call_Zmmrs64LFscpAHsIUaIfEcAN", - "id": "fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_Zmmrs64LFscpAHsIUaIfEcAN", - "output": "total 4\ndrwxr-xr-x 2 vercel-sandbox vercel-sandbox 22 Aug 3 07:55 .\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 07:55 ..\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 194 Aug 3 07:55 SKILL.md\n./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6d008c81a3a332b5376350158b\",\"object\":\"response\",\"created_at\":1785748333,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6d008c81a3a332b5376350158b\",\"object\":\"response\",\"created_at\":1785748333,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_KXcpmIy4Cbfugk4SNT2Ge7sG\",\"name\":\"read\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"AplT1bUEIbLxZO\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"path\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"m2vm6uJH1fpp\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"UE4B3xJGoZjFiD\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"vXAYv4q4OGZeBm\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"lZr7JtLSOpK81\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"AlOhsp6mt6WsL\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"5nqW1Lb0h9tswT\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"A7di1P36tJ\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"sXrATqCJ4vWOD1I\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"dOlnaweYHcH\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"1e9JdY1CZrh\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"ItoYfCrGd190\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"U2GDfehNMcQdTxK\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"3sMRftpfAWB\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"RtP3uejwoaW\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"WTDkaRqKEdlO\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"Sicm3wGutAJOUR\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"i3Iuw9L\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"cNyMn1ymq3OidT\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"TF4UthkOjNccn7Q\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"WCH0Nr5syDCGWhy\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"9QxeDK80jR88i\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"ZWEwRV5Aa\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"GCLbXJEBfam0u\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"fYviPD7BD1YoJ7\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"12G2xIB4r4pGx\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"RBo7r0Kw\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"LdN0iq6VGNg\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/.\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"SC3tJji0aA9vxI\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"agents\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"46FVinGMH1\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"Tu4YeGAIlzJv7RO\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"skills\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"chyXB4Vg5n\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"koR43IjON9fzgj\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"2PamefwKxxhd235\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"7OGNeIaOwukKH4k\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"LyzjvJ7oVbrNO\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"F6FvzwFZ7LJK05\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"PvR9f24G2kgLxp\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"kill\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"Cc4f9PrTB9gy\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/S\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"NFmNKyCIgJRpgX\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"K\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"i6Hkkg289FwqvEa\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ILL\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"Z0w7G2ePBPqRw\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".md\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"U8sCPOgqSHwmL\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"Rj1PgiaJnVY9e\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"offset\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"Nv7VtoPO8D\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"xMA7iDRhGXyoRS\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"1\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"4HmQwBvniK7325Y\",\"output_index\":0,\"sequence_number\":49}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"fp7VCNlkmETdHR\",\"output_index\":0,\"sequence_number\":50}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"limit\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"JnAHVomsUGV\",\"output_index\":0,\"sequence_number\":51}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"oMhJTWBoaVQje5\",\"output_index\":0,\"sequence_number\":52}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"200\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"pck09M2RCSfdF\",\"output_index\":0,\"sequence_number\":53}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"obfuscation\":\"v0WZruprCkbMb5J\",\"output_index\":0,\"sequence_number\":54}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"path\\\":\\\"./.agents/skills/e2e-flue-skill/SKILL.md\\\",\\\"offset\\\":1,\\\"limit\\\":200}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"output_index\":0,\"sequence_number\":55}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"path\\\":\\\"./.agents/skills/e2e-flue-skill/SKILL.md\\\",\\\"offset\\\":1,\\\"limit\\\":200}\",\"call_id\":\"call_KXcpmIy4Cbfugk4SNT2Ge7sG\",\"name\":\"read\"},\"output_index\":0,\"sequence_number\":56}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6d008c81a3a332b5376350158b\",\"object\":\"response\",\"created_at\":1785748333,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748334,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"path\\\":\\\"./.agents/skills/e2e-flue-skill/SKILL.md\\\",\\\"offset\\\":1,\\\"limit\\\":200}\",\"call_id\":\"call_KXcpmIy4Cbfugk4SNT2Ge7sG\",\"name\":\"read\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1762,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":1536},\"output_tokens\":64,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1826},\"user\":null,\"metadata\":{}},\"sequence_number\":57}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a2543308de867bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:13 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "248", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179997783", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_33374d00629445b4aa73562f25cf18f0" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 12, - "id": "fecb7c9f8d04e1bd", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:15.213Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -name AGENTS.md -print\",\"timeout\":100000}", - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "id": "fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "output": "total 4\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 09:10 .\ndrwxr-xr-x 10 vercel-sandbox vercel-sandbox 4096 Aug 3 09:11 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 09:10 skills", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\;\",\"timeout\":100000}", - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "id": "fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\",\"timeout\":100000}", - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "id": "fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head\",\"timeout\":100000}", - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "id": "fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "output": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print\",\"timeout\":100000}", - "call_id": "call_Zmmrs64LFscpAHsIUaIfEcAN", - "id": "fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_Zmmrs64LFscpAHsIUaIfEcAN", - "output": "total 4\ndrwxr-xr-x 2 vercel-sandbox vercel-sandbox 22 Aug 3 07:55 .\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 07:55 ..\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 194 Aug 3 07:55 SKILL.md\n./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "function_call_output" - }, - { - "arguments": "{\"path\":\"./.agents/skills/e2e-flue-skill/SKILL.md\",\"offset\":1,\"limit\":200}", - "call_id": "call_KXcpmIy4Cbfugk4SNT2Ge7sG", - "id": "fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997", - "name": "read", - "type": "function_call" - }, - { - "call_id": "call_KXcpmIy4Cbfugk4SNT2Ge7sG", - "output": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6e34cc81a383184de2af428d09\",\"object\":\"response\",\"created_at\":1785748334,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6e34cc81a383184de2af428d09\",\"object\":\"response\",\"created_at\":1785748334,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_xQybhnQYuflmfp7gyAScVKjY\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"IsfKDqHlaKpPlc\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"command\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"05wTcU2vE\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"I7n2P9cIUaGZ5\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"node\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"66No8n2S3lQV\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"pfQfFqmrd7Cttd\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"v\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"MsjXV30TNBXaVIN\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" &&\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"sbVmMrYzWCmWN\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" ls\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"f3AOiA6bYx0WA\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" -\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"YwMIVfxBwLPyPz\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"la\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"yosVAZ9d354t0E\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"RtzYTt0vUd1KL5\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"UZCqFJvIdZJfi\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"ob8WdL1Zrd6z4\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"SHFL6SDvDru2gO\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"SlANW27QBQ\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"Llwwd75yCVN6cFY\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"Sp2PfwThtXK\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"WTr9EL0CMfU\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"iXd0y36BKeMz\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"TMVJ4avBWNU1PMg\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"gGYEjs7floV\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"nJfWpNMEeYh\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"VnnFujipwf0B\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"AAXM8t27HpiY8a\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"51XrioX\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"zAaUUwSEMO3Bxs\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"vyBpS3fu1undfoT\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"myDkZQqFtQPgErv\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"7jYdsKtAqGJsx\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"FgfnvD0oV\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"ECM3KkIZxcc2j\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"XIzDZYr4JpAjhV\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"VudZPLnjNH5PJ\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"i2iTyJpP\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"ZtblFC8j0Wk\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" &&\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"1l1tQ40NrGj8E\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" cat\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"41U8EU6Ib9cg\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"cglsLI76yH9ClQ\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"qvk54uq2LZnZa\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"aovMJj1uFOFEM\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"NQpTAggULtv8zO\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"5837PUGg7V\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"JhMbsWIyEoPXZRB\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"wUKGqlUZBXL\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"5fqXYCtg0Gt\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"4Q1cI53ucyWL\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"abd5F6mWTBD3FqR\",\"output_index\":0,\"sequence_number\":49}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"YwaHJecYa6Z\",\"output_index\":0,\"sequence_number\":50}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"IZiyFBgwGNk\",\"output_index\":0,\"sequence_number\":51}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"DefferqgDs1G\",\"output_index\":0,\"sequence_number\":52}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"jf9cr3akDjVA60\",\"output_index\":0,\"sequence_number\":53}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"XaGvBNu\",\"output_index\":0,\"sequence_number\":54}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"5UiGHyP1PGgpsP\",\"output_index\":0,\"sequence_number\":55}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"WKaJqwMQWdtBVYa\",\"output_index\":0,\"sequence_number\":56}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"DiOuTdnwYCCUpLq\",\"output_index\":0,\"sequence_number\":57}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"JNIox4EYrQqid\",\"output_index\":0,\"sequence_number\":58}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"rQexF5fiq\",\"output_index\":0,\"sequence_number\":59}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"CxjB3QQkwwxPq\",\"output_index\":0,\"sequence_number\":60}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"KvqTdaudMieVrZ\",\"output_index\":0,\"sequence_number\":61}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"hqrEuju9Hubvj\",\"output_index\":0,\"sequence_number\":62}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"xqdV3tk5\",\"output_index\":0,\"sequence_number\":63}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"finNWbOcOSK\",\"output_index\":0,\"sequence_number\":64}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/package\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"w5O538dR\",\"output_index\":0,\"sequence_number\":65}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".json\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"5lixmLK9osS\",\"output_index\":0,\"sequence_number\":66}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" \",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"UfcwAy15pK3UyDX\",\"output_index\":0,\"sequence_number\":67}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"8kuEminD3O9nEe6\",\"output_index\":0,\"sequence_number\":68}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\">/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"0ujGWvOIOaHg9t\",\"output_index\":0,\"sequence_number\":69}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"dev\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"l87mZp3YVC56A\",\"output_index\":0,\"sequence_number\":70}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/null\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"z1OHwKMfyTN\",\"output_index\":0,\"sequence_number\":71}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" ||\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"Nd0PVbdoXgVdP\",\"output_index\":0,\"sequence_number\":72}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" true\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"gYO8swE9D00\",\"output_index\":0,\"sequence_number\":73}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"dAlT06VEGUSd4\",\"output_index\":0,\"sequence_number\":74}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"timeout\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"9zDDLkKve\",\"output_index\":0,\"sequence_number\":75}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"bOgN2cbK9QPYUV\",\"output_index\":0,\"sequence_number\":76}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"100\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"2ycAaEUguB849\",\"output_index\":0,\"sequence_number\":77}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"000\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"jCkJ68Rcb58dt\",\"output_index\":0,\"sequence_number\":78}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"obfuscation\":\"bbF8wOnG3Zvtgvs\",\"output_index\":0,\"sequence_number\":79}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"command\\\":\\\"cat ./package.json\\\",\\\"timeout\\\":100000}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"output_index\":0,\"sequence_number\":80}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"cat ./package.json\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_xQybhnQYuflmfp7gyAScVKjY\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":81}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6e34cc81a383184de2af428d09\",\"object\":\"response\",\"created_at\":1785748334,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748335,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"cat ./package.json\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_xQybhnQYuflmfp7gyAScVKjY\",\"name\":\"bash\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":1882,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":1536},\"output_tokens\":90,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1972},\"user\":null,\"metadata\":{}},\"sequence_number\":82}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25433108e2d7bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:14 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "170", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179997663", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_be4542a23d5f4e17b4285d598ac2400b" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 13, - "id": "eebc152ba05b8412", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:16.175Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -name AGENTS.md -print\",\"timeout\":100000}", - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "id": "fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "output": "total 4\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 09:10 .\ndrwxr-xr-x 10 vercel-sandbox vercel-sandbox 4096 Aug 3 09:11 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 09:10 skills", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\;\",\"timeout\":100000}", - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "id": "fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\",\"timeout\":100000}", - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "id": "fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head\",\"timeout\":100000}", - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "id": "fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "output": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print\",\"timeout\":100000}", - "call_id": "call_Zmmrs64LFscpAHsIUaIfEcAN", - "id": "fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_Zmmrs64LFscpAHsIUaIfEcAN", - "output": "total 4\ndrwxr-xr-x 2 vercel-sandbox vercel-sandbox 22 Aug 3 07:55 .\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 07:55 ..\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 194 Aug 3 07:55 SKILL.md\n./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "function_call_output" - }, - { - "arguments": "{\"path\":\"./.agents/skills/e2e-flue-skill/SKILL.md\",\"offset\":1,\"limit\":200}", - "call_id": "call_KXcpmIy4Cbfugk4SNT2Ge7sG", - "id": "fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997", - "name": "read", - "type": "function_call" - }, - { - "call_id": "call_KXcpmIy4Cbfugk4SNT2Ge7sG", - "output": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"cat ./package.json\",\"timeout\":100000}", - "call_id": "call_xQybhnQYuflmfp7gyAScVKjY", - "id": "fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_xQybhnQYuflmfp7gyAScVKjY", - "output": "v24.14.1\ntotal 244\ndrwxr-xr-x 7 vercel-sandbox vercel-sandbox 4096 Aug 3 09:06 .\ndrwxr-xr-x 46 vercel-sandbox vercel-sandbox 4096 Aug 3 07:55 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 07:55 .agents\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 38 Aug 3 07:55 .flue\ndrwxr-xr-x 2 vercel-sandbox vercel-sandbox 218 Aug 3 09:11 __cassettes__\ndrwxr-xr-x 2 vercel-sandbox vercel-sandbox 4096 Aug 3 08:49 __snapshots__\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 12484 Aug 3 08:45 assertions.ts\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 380 Aug 3 07:55 cassette-filter.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 215 Aug 3 07:55 constants.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 2000 Aug 3 09:06 package.json\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 167098 Aug 3 09:06 pnpm-lock.yaml\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 123 Aug 3 07:55 scenario.cli.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 6877 Aug 3 09:06 scenario.impl.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 203 Aug 3 07:55 scenario.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 8066 Aug 3 09:09 scenario.test.ts\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 121 Aug 3 07:55 scenario.ts\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 2376 Aug 3 08:49 scenario.v2.ts\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 4616 Aug 3 08:50 v2-assertions.ts\ndrwxr-xr-x 4 vercel-sandbox vercel-sandbox 26 Aug 3 08:06 versions\n{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.4\": \"^3.1.4\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.8\": \"^5.0.8\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\"\n }\n }\n}", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6f493c81a3a225e3a6b86bbee9\",\"object\":\"response\",\"created_at\":1785748335,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6f493c81a3a225e3a6b86bbee9\",\"object\":\"response\",\"created_at\":1785748335,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_7fRJCCDIZe4x17WaroXZwmjV\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"fHq8b6uDNpdKyA\",\"output_index\":0,\"sequence_number\":3}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"command\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"thvsscitV\",\"output_index\":0,\"sequence_number\":4}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"4KhNnIoFmUxL4\",\"output_index\":0,\"sequence_number\":5}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cd\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"2MAKSqM5RZc02D\",\"output_index\":0,\"sequence_number\":6}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" /\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"rtJwB0qEFcuGP0\",\"output_index\":0,\"sequence_number\":7}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ver\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"F1lTsvrOqKTGW\",\"output_index\":0,\"sequence_number\":8}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"cel\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"OW6Ho3Im2OXeD\",\"output_index\":0,\"sequence_number\":9}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/s\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"OeETspVfDui6qp\",\"output_index\":0,\"sequence_number\":10}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"andbox\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"TYKoEchhWA\",\"output_index\":0,\"sequence_number\":11}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"mlWpUOMOoFsxvPZ\",\"output_index\":0,\"sequence_number\":12}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"dhWAHpJ9eAO\",\"output_index\":0,\"sequence_number\":13}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"obL0WnjlMLS\",\"output_index\":0,\"sequence_number\":14}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"data\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"VQtHA1VRMGyD\",\"output_index\":0,\"sequence_number\":15}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"v3crkHcTsSeS6g9\",\"output_index\":0,\"sequence_number\":16}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"brain\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"fLyFqifDKnd\",\"output_index\":0,\"sequence_number\":17}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"zOZwoEc9EwU\",\"output_index\":0,\"sequence_number\":18}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-sdk\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"jKM1tjLK0Nox\",\"output_index\":0,\"sequence_number\":19}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-j\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"N8hk8Xm3OX7w2R\",\"output_index\":0,\"sequence_number\":20}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"avascript\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"P8EgPwQ\",\"output_index\":0,\"sequence_number\":21}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"aIWWYDGHFlbnf0\",\"output_index\":0,\"sequence_number\":22}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"k9oYAIfrrIU8RoN\",\"output_index\":0,\"sequence_number\":23}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"qYFO9k1cYtpeWCN\",\"output_index\":0,\"sequence_number\":24}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/sc\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"h3KfOXIs1dENs\",\"output_index\":0,\"sequence_number\":25}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"enarios\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"JH679LrpQ\",\"output_index\":0,\"sequence_number\":26}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/fl\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"gMCaFOZiRoUg3\",\"output_index\":0,\"sequence_number\":27}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ue\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"yXl3sFNxeXVgqy\",\"output_index\":0,\"sequence_number\":28}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"-in\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"glJkNUpBDKD1z\",\"output_index\":0,\"sequence_number\":29}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"VBlCVyqb\",\"output_index\":0,\"sequence_number\":30}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"SNMP91Q3rDW\",\"output_index\":0,\"sequence_number\":31}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" &&\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"X3ihmCKcYuVXU\",\"output_index\":0,\"sequence_number\":32}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" node\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"JKpFNiOkxT3\",\"output_index\":0,\"sequence_number\":33}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" scenario\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"bFvkMJJ\",\"output_index\":0,\"sequence_number\":34}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".cli\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"kUiDpbheCAHH\",\"output_index\":0,\"sequence_number\":35}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".m\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"FxsGZq8BCGo3gl\",\"output_index\":0,\"sequence_number\":36}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"js\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"Dp4QtMwr7c3BK1\",\"output_index\":0,\"sequence_number\":37}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" --\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"PeegSlydvrC1L\",\"output_index\":0,\"sequence_number\":38}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"marker\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"wvkAVwRdNi\",\"output_index\":0,\"sequence_number\":39}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" SK\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"PTHJb9FqWdVqt\",\"output_index\":0,\"sequence_number\":40}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ILL\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"2fQZxFddpb4N0\",\"output_index\":0,\"sequence_number\":41}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"_DONE\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"k70XHwhwure\",\"output_index\":0,\"sequence_number\":42}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\",\\\"\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"rGVgz0vPyy7dV\",\"output_index\":0,\"sequence_number\":43}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"timeout\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"MAkwSc4cj\",\"output_index\":0,\"sequence_number\":44}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"RnIym4ATTjxwrH\",\"output_index\":0,\"sequence_number\":45}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"100\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"soJ0BJsFSpsnc\",\"output_index\":0,\"sequence_number\":46}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"000\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"DQKN9g0JhSsmY\",\"output_index\":0,\"sequence_number\":47}", - "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"obfuscation\":\"BTOAjj33gQRbswj\",\"output_index\":0,\"sequence_number\":48}", - "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"command\\\":\\\"printf SKILL_DONE\\\",\\\"timeout\\\":100000}\",\"item_id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"output_index\":0,\"sequence_number\":49}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"printf SKILL_DONE\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_7fRJCCDIZe4x17WaroXZwmjV\",\"name\":\"bash\"},\"output_index\":0,\"sequence_number\":50}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b6f493c81a3a225e3a6b86bbee9\",\"object\":\"response\",\"created_at\":1785748335,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748336,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"command\\\":\\\"printf SKILL_DONE\\\",\\\"timeout\\\":100000}\",\"call_id\":\"call_7fRJCCDIZe4x17WaroXZwmjV\",\"name\":\"bash\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":3421,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":1536},\"output_tokens\":59,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":3480},\"user\":null,\"metadata\":{}},\"sequence_number\":51}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a25433173b9c7bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:15 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "200", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179996124", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "1ms", - "x-request-id": "req_3d947e9c4a9c4ea6a016b86fe1407743" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 14, - "id": "bc08e707f2920073", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:16.776Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run e2e-flue-skill with marker SKILL_DONE\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Ensure completion and report the final output marker only.\",\"agent\":\"e2e-flue-skill\",\"cwd\":\".\"}", - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "id": "fc_054a16a2f2ea3217016a705b64fe5481a390185c9576df12b6", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_w6pyYZgnhlSOAXul75DAG1Cj", - "output": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -name AGENTS.md -print\",\"timeout\":100000}", - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "id": "fc_054a16a2f2ea3217016a705b66460481a3a5de06df603e35ee", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_6zEBnJRmXFIkGFwNjWLZP4uC", - "output": "total 4\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 09:10 .\ndrwxr-xr-x 10 vercel-sandbox vercel-sandbox 4096 Aug 3 09:11 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 09:10 skills", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\\\;\",\"timeout\":100000}", - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "id": "fc_054a16a2f2ea3217016a705b67575881a3aacde02b9053a321", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_jQUCie0ZgaXUrZCDcezp0msl", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 6 -type f -name 'AGENTS.md' -print\",\"timeout\":100000}", - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "id": "fc_054a16a2f2ea3217016a705b68c39881a3b1e518599236e41a", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_NgMSUb6wIR0GAf9U8LXERtBQ", - "output": "(no output)", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head\",\"timeout\":100000}", - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "id": "fc_054a16a2f2ea3217016a705b69edd081a38c31583ec56eca5f", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_BXKiQ5KG8c42XPPp94zgcY3H", - "output": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print\",\"timeout\":100000}", - "call_id": "call_Zmmrs64LFscpAHsIUaIfEcAN", - "id": "fc_054a16a2f2ea3217016a705b6b96cc81a3ac4761db24198a63", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_Zmmrs64LFscpAHsIUaIfEcAN", - "output": "total 4\ndrwxr-xr-x 2 vercel-sandbox vercel-sandbox 22 Aug 3 07:55 .\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 28 Aug 3 07:55 ..\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 194 Aug 3 07:55 SKILL.md\n./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "function_call_output" - }, - { - "arguments": "{\"path\":\"./.agents/skills/e2e-flue-skill/SKILL.md\",\"offset\":1,\"limit\":200}", - "call_id": "call_KXcpmIy4Cbfugk4SNT2Ge7sG", - "id": "fc_054a16a2f2ea3217016a705b6d71cc81a38ac06634a309a997", - "name": "read", - "type": "function_call" - }, - { - "call_id": "call_KXcpmIy4Cbfugk4SNT2Ge7sG", - "output": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"cat ./package.json\",\"timeout\":100000}", - "call_id": "call_xQybhnQYuflmfp7gyAScVKjY", - "id": "fc_054a16a2f2ea3217016a705b6e90f481a39bc44496fbadf441", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_xQybhnQYuflmfp7gyAScVKjY", - "output": "v24.14.1\ntotal 244\ndrwxr-xr-x 7 vercel-sandbox vercel-sandbox 4096 Aug 3 09:06 .\ndrwxr-xr-x 46 vercel-sandbox vercel-sandbox 4096 Aug 3 07:55 ..\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 20 Aug 3 07:55 .agents\ndrwxr-xr-x 3 vercel-sandbox vercel-sandbox 38 Aug 3 07:55 .flue\ndrwxr-xr-x 2 vercel-sandbox vercel-sandbox 218 Aug 3 09:11 __cassettes__\ndrwxr-xr-x 2 vercel-sandbox vercel-sandbox 4096 Aug 3 08:49 __snapshots__\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 12484 Aug 3 08:45 assertions.ts\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 380 Aug 3 07:55 cassette-filter.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 215 Aug 3 07:55 constants.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 2000 Aug 3 09:06 package.json\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 167098 Aug 3 09:06 pnpm-lock.yaml\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 123 Aug 3 07:55 scenario.cli.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 6877 Aug 3 09:06 scenario.impl.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 203 Aug 3 07:55 scenario.mjs\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 8066 Aug 3 09:09 scenario.test.ts\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 121 Aug 3 07:55 scenario.ts\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 2376 Aug 3 08:49 scenario.v2.ts\n-rw-r--r-- 1 vercel-sandbox vercel-sandbox 4616 Aug 3 08:50 v2-assertions.ts\ndrwxr-xr-x 4 vercel-sandbox vercel-sandbox 26 Aug 3 08:06 versions\n{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.4\": \"^3.1.4\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.8\": \"^5.0.8\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\"\n }\n }\n}", - "type": "function_call_output" - }, - { - "arguments": "{\"command\":\"printf SKILL_DONE\",\"timeout\":100000}", - "call_id": "call_7fRJCCDIZe4x17WaroXZwmjV", - "id": "fc_054a16a2f2ea3217016a705b6fb64c81a38ff895c9553c8763", - "name": "bash", - "type": "function_call" - }, - { - "call_id": "call_7fRJCCDIZe4x17WaroXZwmjV", - "output": "node:internal/modules/package_json_reader:301\n throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);\n ^\n\nError [ERR_MODULE_NOT_FOUND]: Cannot find package '@flue/cli' imported from ./scenario.impl.mjs\n at Object.getPackageJSONURL (node:internal/modules/package_json_reader:301:9)\n at packageResolve (node:internal/modules/esm/resolve:768:81)\n at moduleResolve (node:internal/modules/esm/resolve:859:18)\n at defaultResolve (node:internal/modules/esm/resolve:991:11)\n at #cachedDefaultResolve (node:internal/modules/esm/loader:719:20)\n at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:736:38)\n at ModuleLoader.resolveSync (node:internal/modules/esm/loader:765:52)\n at Object.resolve (node:internal/modules/esm/initialize_import_meta:37:25)\n at file://./scenario.impl.mjs:12:33\n at ModuleJob.run (node:internal/modules/esm/module_job:430:25) {\n code: 'ERR_MODULE_NOT_FOUND'\n}\n\nNode.js v24.14.1\n\nCommand exited with code 1", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b70474881a39a050b8889ca15cd\",\"object\":\"response\",\"created_at\":1785748336,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b70474881a39a050b8889ca15cd\",\"object\":\"response\",\"created_at\":1785748336,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"SK\",\"item_id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"logprobs\":[],\"obfuscation\":\"M9SWfj095W22jm\",\"output_index\":0,\"sequence_number\":4}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ILL\",\"item_id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"logprobs\":[],\"obfuscation\":\"NnKtLGfgvufIN\",\"output_index\":0,\"sequence_number\":5}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"_DONE\",\"item_id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"logprobs\":[],\"obfuscation\":\"wHpr2r6Jc26\",\"output_index\":0,\"sequence_number\":6}", - "event: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":7,\"text\":\"SKILL_DONE\"}", - "event: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"SKILL_DONE\"},\"sequence_number\":8}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"SKILL_DONE\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":9}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_054a16a2f2ea3217016a705b70474881a39a050b8889ca15cd\",\"object\":\"response\",\"created_at\":1785748336,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748336,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"SKILL_DONE\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":3812,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":2560},\"output_tokens\":7,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":3819},\"user\":null,\"metadata\":{}},\"sequence_number\":10}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a254331d7fe07bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:16 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "201", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179995734", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "1ms", - "x-request-id": "req_6dc67fd2f6124704a9d86e5e918bac37" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 15, - "id": "b3f219cac4c52d6c", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:17.515Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "developer" - }, - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "input_text" - } - ], - "role": "user" - }, - { - "arguments": "{\"description\":\"Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.\",\"prompt\":\"Run the skill named \\\"e2e-flue-skill\\\" with arguments {\\\"marker\\\":\\\"SKILL_DONE\\\"}. Output only the marker and no extra text when finished.\",\"cwd\":\".\"}", - "call_id": "call_R2a5LOml0NZqq5XTcTCKfbMW", - "id": "fc_0f099b3fce0721fd016a705b63c99081a3af4dfbf2ce0314eb", - "name": "task", - "type": "function_call" - }, - { - "call_id": "call_R2a5LOml0NZqq5XTcTCKfbMW", - "output": "SKILL_DONE", - "type": "function_call_output" - } - ], - "model": "gpt-5.4-nano", - "prompt_cache_key": "[REDACTED]", - "reasoning": { - "effort": "none" - }, - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0f099b3fce0721fd016a705b70d65881a3b5bc873ea54e7c00\",\"object\":\"response\",\"created_at\":1785748336,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::sk\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0f099b3fce0721fd016a705b70d65881a3b5bc873ea54e7c00\",\"object\":\"response\",\"created_at\":1785748336,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::sk\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"SK\",\"item_id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"logprobs\":[],\"obfuscation\":\"iSZ7rWanncJQ68\",\"output_index\":0,\"sequence_number\":4}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ILL\",\"item_id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"logprobs\":[],\"obfuscation\":\"PQly4lHaqRXSu\",\"output_index\":0,\"sequence_number\":5}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"_DONE\",\"item_id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"logprobs\":[],\"obfuscation\":\"WSIPPwqqidY\",\"output_index\":0,\"sequence_number\":6}", - "event: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":7,\"text\":\"SKILL_DONE\"}", - "event: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"SKILL_DONE\"},\"sequence_number\":8}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"SKILL_DONE\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":9}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0f099b3fce0721fd016a705b70d65881a3b5bc873ea54e7c00\",\"object\":\"response\",\"created_at\":1785748336,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748337,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-nano-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"SKILL_DONE\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::sk\",\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":969,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":7,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":976},\"user\":null,\"metadata\":{}},\"sequence_number\":10}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a2543320eec07bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:17 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "226", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179998575", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_0f63631954824ce89d221047a6871115" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 16, - "id": "eea2c2f028018d92", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:18.781Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.\n\nYou are a deterministic Flue instrumentation test agent. Follow user instructions exactly. When asked for a marker, output only that marker and no extra text. When running a local skill file, read it yourself and do not delegate it to a task.\n\n## Available Skills\n\nThe following skills provide specialized instructions for specific tasks. When a task matches a skill description, activate that skill before proceeding so its full instructions are loaded. Skill instructions and supporting resources stay lazy until activation or explicit file reads.\n\n- **e2e-flue-skill** — Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n\nDate: Mon, Aug 3, 2026\nWorking directory: .\n\nDirectory structure:\n.agents\n.flue\n.flue-build\n__cassettes__\n__snapshots__\nassertions.ts\ncassette-filter.mjs\nconstants.mjs\ndist\nnode_modules\npackage.json\npnpm-lock.yaml\nscenario.cli.mjs\nscenario.impl.mjs\nscenario.mjs\nscenario.test.ts\nscenario.ts\nscenario.v2.ts\nv2-assertions.ts\nversions", - "role": "system" - }, - { - "content": [ - { - "text": "Reply with exactly TASK_DONE and no other text.", - "type": "input_text" - } - ], - "role": "user" - } - ], - "model": "gpt-4o-mini", - "prompt_cache_key": "[REDACTED]", - "store": false, - "stream": true, - "tools": [ - { - "description": "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.", - "name": "read", - "parameters": { - "properties": { - "limit": { - "description": "Maximum number of lines to read", - "type": "number" - }, - "offset": { - "description": "Line number to start from (1-indexed)", - "type": "number" - }, - "path": { - "description": "Path to the file to read", - "type": "string" - } - }, - "required": ["path"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Write content to a file. Creates the file and parent directories if they do not exist.", - "name": "write", - "parameters": { - "properties": { - "content": { - "description": "Content to write to the file", - "type": "string" - }, - "path": { - "description": "Path to the file to write", - "type": "string" - } - }, - "required": ["path", "content"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.", - "name": "edit", - "parameters": { - "properties": { - "newText": { - "description": "Replacement text", - "type": "string" - }, - "oldText": { - "description": "Exact text to find (must be unique)", - "type": "string" - }, - "path": { - "description": "Path to the file to edit", - "type": "string" - }, - "replaceAll": { - "description": "Replace all occurrences", - "type": "boolean" - } - }, - "required": ["path", "oldText", "newText"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.", - "name": "bash", - "parameters": { - "properties": { - "command": { - "description": "Bash command to execute", - "type": "string" - }, - "timeout": { - "description": "Timeout in seconds", - "type": "number" - } - }, - "required": ["command"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.", - "name": "grep", - "parameters": { - "properties": { - "include": { - "description": "Glob filter, e.g. \"*.ts\"", - "type": "string" - }, - "path": { - "description": "Directory or file to search (default: .)", - "type": "string" - }, - "pattern": { - "description": "Search pattern (regex)", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Find files by filename pattern using shell find -name semantics. Returns matching file paths.", - "name": "glob", - "parameters": { - "properties": { - "path": { - "description": "Directory to search in (default: .)", - "type": "string" - }, - "pattern": { - "description": "Filename pattern, e.g. \"*.ts\"", - "type": "string" - } - }, - "required": ["pattern"], - "type": "object" - }, - "strict": false, - "type": "function" - }, - { - "description": "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.", - "name": "task", - "parameters": { - "properties": { - "agent": { - "description": "Declared subagent to use for the child agent", - "type": "string" - }, - "cwd": { - "description": "Working directory for the child agent. AGENTS.md and skills are discovered from here.", - "type": "string" - }, - "description": { - "description": "Short human-readable label for the delegated work", - "type": "string" - }, - "prompt": { - "description": "Focused instructions for the child agent", - "type": "string" - } - }, - "required": ["prompt"], - "type": "object" - }, - "strict": false, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0f3adfdb1510c165016a705b7192bc81a094e1dbada8d8b42b\",\"object\":\"response\",\"created_at\":1785748337,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0f3adfdb1510c165016a705b7192bc81a094e1dbada8d8b42b\",\"object\":\"response\",\"created_at\":1785748337,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"TASK\",\"item_id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\",\"logprobs\":[],\"obfuscation\":\"mlb5p2sBa8Nm\",\"output_index\":0,\"sequence_number\":4}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"_DONE\",\"item_id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\",\"logprobs\":[],\"obfuscation\":\"oWnwIAUMpIA\",\"output_index\":0,\"sequence_number\":5}", - "event: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":6,\"text\":\"TASK_DONE\"}", - "event: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"TASK_DONE\"},\"sequence_number\":7}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"TASK_DONE\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":8}", - "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0f3adfdb1510c165016a705b7192bc81a094e1dbada8d8b42b\",\"object\":\"response\",\"created_at\":1785748337,\"status\":\"completed\",\"background\":false,\"completed_at\":1785748338,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"TASK_DONE\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":\"workflow:instrumentation:01KZ3E9TWTAEK4P4P3F63HKAKC::default::ta\",\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.\",\"name\":\"read\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to read\"},\"offset\":{\"type\":\"number\",\"description\":\"Line number to start from (1-indexed)\"},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to read\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Write content to a file. Creates the file and parent directories if they do not exist.\",\"name\":\"write\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"content\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to write\"},\"content\":{\"type\":\"string\",\"description\":\"Content to write to the file\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.\",\"name\":\"edit\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"path\",\"oldText\",\"newText\"],\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file to edit\"},\"oldText\":{\"type\":\"string\",\"description\":\"Exact text to find (must be unique)\"},\"newText\":{\"type\":\"string\",\"description\":\"Replacement text\"},\"replaceAll\":{\"type\":\"boolean\",\"description\":\"Replace all occurrences\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.\",\"name\":\"bash\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"command\"],\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"Bash command to execute\"},\"timeout\":{\"type\":\"number\",\"description\":\"Timeout in seconds\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.\",\"name\":\"grep\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Search pattern (regex)\"},\"path\":{\"type\":\"string\",\"description\":\"Directory or file to search (default: .)\"},\"include\":{\"type\":\"string\",\"description\":\"Glob filter, e.g. \\\"*.ts\\\"\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Find files by filename pattern using shell find -name semantics. Returns matching file paths.\",\"name\":\"glob\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"pattern\"],\"properties\":{\"pattern\":{\"type\":\"string\",\"description\":\"Filename pattern, e.g. \\\"*.ts\\\"\"},\"path\":{\"type\":\"string\",\"description\":\"Directory to search in (default: .)\"}}},\"strict\":false},{\"type\":\"function\",\"description\":\"Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation. No subagents are currently defined.\",\"name\":\"task\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Short human-readable label for the delegated work\"},\"prompt\":{\"type\":\"string\",\"description\":\"Focused instructions for the child agent\"},\"agent\":{\"type\":\"string\",\"description\":\"Declared subagent to use for the child agent\"},\"cwd\":{\"type\":\"string\",\"description\":\"Working directory for the child agent. AGENTS.md and skills are discovered from here.\"}}},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":826,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":4,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":830},\"user\":null,\"metadata\":{}},\"sequence_number\":9}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a254332588917bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:17 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "169", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-request-id": "req_44ff6ef3357a4d78a0e4b836a0777717" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 17, - "id": "dd8319160ba3e4cf", - "matchKey": "POST api.openai.com/v1/responses", - "recordedAt": "2026-08-03T09:12:19.721Z", - "request": { - "body": { - "kind": "json", - "value": { - "input": [ - { - "content": "You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.", - "role": "system" - }, - { - "content": [ - { - "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", - "type": "input_text" - } - ], - "role": "user" - } - ], - "max_output_tokens": 32, - "model": "gpt-4o-mini", - "store": false, - "stream": true - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/responses" - }, - "response": { - "body": { - "chunks": [ - "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_04d28ba03840bb65016a705b72d36081a3a387e30f2739466c\",\"object\":\"response\",\"created_at\":1785748338,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":32,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", - "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_04d28ba03840bb65016a705b72d36081a3a387e30f2739466c\",\"object\":\"response\",\"created_at\":1785748338,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":32,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", - "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}", - "event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"##\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"pnRDruuzK4zmSi\",\"output_index\":0,\"sequence_number\":4}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Original\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"xeQeZBG\",\"output_index\":0,\"sequence_number\":5}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Request\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"wDl6Xfa7\",\"output_index\":0,\"sequence_number\":6}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"\\n\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"SPfPR8TAgZd37wg\",\"output_index\":0,\"sequence_number\":7}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"The\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"cgHwyTaOZmFtZ\",\"output_index\":0,\"sequence_number\":8}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" user\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"1z0yUfwbdDc\",\"output_index\":0,\"sequence_number\":9}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" requested\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"eCqgHs\",\"output_index\":0,\"sequence_number\":10}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" a\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"7RAJkk4N6FYOim\",\"output_index\":0,\"sequence_number\":11}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" step\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"y7scL3Od23b\",\"output_index\":0,\"sequence_number\":12}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"-by\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"W7lrv8VHEOIfi\",\"output_index\":0,\"sequence_number\":13}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"-step\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"4LIgLNAJAB0\",\"output_index\":0,\"sequence_number\":14}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" execution\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"F077yL\",\"output_index\":0,\"sequence_number\":15}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" of\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"fMOsa5wMUpP8t\",\"output_index\":0,\"sequence_number\":16}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" an\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"SXWFdQci5VvIi\",\"output_index\":0,\"sequence_number\":17}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" instrument\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"IM33q\",\"output_index\":0,\"sequence_number\":18}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ed\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"nO8YVpkRDW8oOQ\",\"output_index\":0,\"sequence_number\":19}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" research\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"xjbeOFx\",\"output_index\":0,\"sequence_number\":20}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" flow\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"PjjjDiy3SBJ\",\"output_index\":0,\"sequence_number\":21}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" involving\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"I58VnM\",\"output_index\":0,\"sequence_number\":22}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" tool\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"i0bH5Hefbkz\",\"output_index\":0,\"sequence_number\":23}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" calls\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"BEMMaOeJJU\",\"output_index\":0,\"sequence_number\":24}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" to\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"gPeC3aszPMi0S\",\"output_index\":0,\"sequence_number\":25}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" gather\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"zlkkxcR5g\",\"output_index\":0,\"sequence_number\":26}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" information\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"7hhs\",\"output_index\":0,\"sequence_number\":27}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" on\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"lLr5hZIXvG8Gb\",\"output_index\":0,\"sequence_number\":28}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" \\\"\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"5qdl5MlY2bxs7F\",\"output_index\":0,\"sequence_number\":29}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"fl\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"yt6z8GDsVV0Qew\",\"output_index\":0,\"sequence_number\":30}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ue\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"3QmzDfnfod8ZRQ\",\"output_index\":0,\"sequence_number\":31}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" instrumentation\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"\",\"output_index\":0,\"sequence_number\":32}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"\\\"\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"yiSjj02o3FbcC4U\",\"output_index\":0,\"sequence_number\":33}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" and\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"796Vjv2p8Npd\",\"output_index\":0,\"sequence_number\":34}", - "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" \\\"\",\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"obfuscation\":\"WkdXPw3c1tG4kc\",\"output_index\":0,\"sequence_number\":35}", - "event: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":36,\"text\":\"## Original Request\\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \\\"flue instrumentation\\\" and \\\"\"}", - "event: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"## Original Request\\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \\\"flue instrumentation\\\" and \\\"\"},\"sequence_number\":37}", - "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"type\":\"message\",\"status\":\"incomplete\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"## Original Request\\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \\\"flue instrumentation\\\" and \\\"\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":38}", - "event: response.incomplete\ndata: {\"type\":\"response.incomplete\",\"response\":{\"id\":\"resp_04d28ba03840bb65016a705b72d36081a3a387e30f2739466c\",\"object\":\"response\",\"created_at\":1785748338,\"status\":\"incomplete\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":{\"reason\":\"max_output_tokens\"},\"instructions\":null,\"max_output_tokens\":32,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\",\"type\":\"message\",\"status\":\"incomplete\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"## Original Request\\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \\\"flue instrumentation\\\" and \\\"\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":1,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":446,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":32,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":478},\"user\":null,\"metadata\":{}},\"sequence_number\":39}" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a254332d68c17bb6-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 03 Aug 2026 09:12:18 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "159", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-request-id": "req_7ea02cb4cc7c43e08845985aea9af023" - }, - "status": 200, - "statusText": "OK" - } - } - ], - "meta": { - "createdAt": "2026-08-03T09:07:11.649Z" - } -} diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.json b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.json deleted file mode 100644 index dd92abe29..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.json +++ /dev/null @@ -1,1448 +0,0 @@ -{ - "span_tree": [ - { - "name": "workflow:instrumentation", - "type": "task", - "children": [ - { - "name": "flue.skill", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 89, - "duration_ms": 0, - "estimated_cost": 0.0041862, - "prompt_cache_creation_tokens": 670, - "prompt_cached_tokens": 1099, - "prompt_tokens": 3, - "tokens": 1861 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/e2e-flue-skill.md" - }, - "output": { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "details": {} - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - }, - "error": "{\"content\":[{\"type\":\"text\",\"text\":\"ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'\"}],\"details\":{}}" - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 52, - "duration_ms": 0, - "estimated_cost": 0.00201795, - "prompt_cache_creation_tokens": 183, - "prompt_cached_tokens": 1769, - "prompt_tokens": 7, - "tokens": 2011 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents" - }, - "output": { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 54, - "duration_ms": 0, - "estimated_cost": 0.0016611, - "prompt_cache_creation_tokens": 66, - "prompt_cached_tokens": 1952, - "prompt_tokens": 6, - "tokens": 2078 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills" - }, - "output": { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents/skills" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 63, - "duration_ms": 0, - "estimated_cost": 0.0018459, - "prompt_cache_creation_tokens": 74, - "prompt_cached_tokens": 2018, - "prompt_tokens": 6, - "tokens": 2161 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills/e2e-flue-skill" - }, - "output": { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents/skills/e2e-flue-skill" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 68, - "duration_ms": 0, - "estimated_cost": 0.0019618500000000002, - "prompt_cache_creation_tokens": 79, - "prompt_cached_tokens": 2092, - "prompt_tokens": 6, - "tokens": 2245 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "output": { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "details": { - "lines": 7, - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 8, - "duration_ms": 0, - "estimated_cost": 0.00129555, - "prompt_cache_creation_tokens": 135, - "prompt_cached_tokens": 2171, - "prompt_tokens": 6, - "tokens": 2320 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "SKILL_DONE", - "metadata": { - "flue.operation": "skill", - "flue.session": "skill", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Reply with exactly TASK_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "TASK_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "task:task:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 4, - "duration_ms": 0, - "estimated_cost": 0.00012299999999999998, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 804, - "tokens": 808 - } - } - ], - "input": "Reply with exactly TASK_DONE and no other text.", - "output": "TASK_DONE", - "metadata": { - "flue.session": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.compact", - "type": "task", - "children": [ - { - "name": "compaction:manual", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant thinking]: The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.\n\n[Assistant]: I'll complete this instrumented research flow step by step. Starting with Step 1:\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant thinking]: Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".\n\n[Assistant]: Now proceeding to Step 2:\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant thinking]: Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.\n\n[Assistant]: Now proceeding to Step 3:\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "## Original Request\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\n\n## Early Progress", - "textSignature": "{\"v\":1,\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "compaction_prefix", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 0, - "duration_ms": 0, - "estimated_cost": 0, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 0, - "tokens": 0 - } - } - ], - "input": { - "estimatedTokens": 2950, - "reason": "manual" - }, - "output": { - "messagesAfter": 2, - "messagesBefore": 8 - }, - "metadata": { - "flue.compaction_reason": "manual", - "flue.session": "main", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0, - "messages_after": 2, - "messages_before": 8 - } - } - ], - "input": { - "estimatedTokens": 2950, - "reason": "manual" - }, - "output": { - "completed": true - }, - "metadata": { - "flue.operation": "compact", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": { - "metadata": { - "scenario": "flue-instrumentation", - "testRunId": "" - }, - "scenario": "flue-instrumentation" - }, - "output": { - "scenario": "flue-instrumentation", - "status": "done" - }, - "metadata": { - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.prompt", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_mode": "full", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 196, - "duration_ms": 0, - "estimated_cost": 0.0113025, - "prompt_cache_creation_tokens": 2222, - "prompt_cached_tokens": 0, - "prompt_tokens": 10, - "tokens": 2428 - } - }, - { - "name": "tool:lookup", - "type": "tool", - "children": [], - "input": { - "query": "flue instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "details": { - "customTool": "lookup" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "lookup", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "lookup" - } - ], - "output": { - "content": [ - { - "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 2:", - "type": "text" - }, - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 158, - "duration_ms": 0, - "estimated_cost": 0.0039606, - "prompt_cache_creation_tokens": 236, - "prompt_cached_tokens": 2222, - "prompt_tokens": 13, - "tokens": 2629 - } - }, - { - "name": "tool:web_search", - "type": "tool", - "children": [], - "input": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "details": { - "customTool": "web_search" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "web_search", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 2:", - "type": "text" - }, - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "web_search" - } - ], - "output": { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 138, - "duration_ms": 0, - "estimated_cost": 0.0037239000000000005, - "prompt_cache_creation_tokens": 234, - "prompt_cached_tokens": 2458, - "prompt_tokens": 13, - "tokens": 2843 - } - }, - { - "name": "tool:summarize_source", - "type": "tool", - "children": [], - "input": { - "url": "https://example.test/flue/reasoning-streams" - }, - "output": { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "details": { - "customTool": "summarize_source" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "summarize_source", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ], - "output": { - "content": [ - { - "thinking": "Perfect! The summarize_source call has completed successfully. According to the instructions, I should now reply with exactly \"PROMPT_DONE\" and no other text.", - "thinkingSignature": "Eu8CCm4IDhgCKkAn2QOIIlYfvcs6ynsmhCslHDtBsrTA4CfmimK7zyz6+k6dZldV2JBtEBET41It0gFcGEKC0n0mAhaYVuLutpDDMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIM1xfLptxSF8rKzou8GgwRLuVmfiBYOT4CZIsiMAYz8TpvchMAWodP+T9uZ+dJ47MVdNv+p86nZXmT5d5EWlCroiAOFHHzJH2o0UjJQSquAX6wu+jIC8/DXwurGIIf2ToldPuJWF5HGj8kHMnwh777XS1NUFl1irM24rvP7kTAv3Dp8YEwb8t3g1EsicY7j+C5DzgwxVsPYCVOCgdMqQjtsqpL7AX5KQ9+hR1gK1DQjshv4Gze3hw/+C+9iptd3RlDIuwO0cm8NY+sVqeWVh3TtQujQlVt1rW/HfWjclrtT0LZSGwc6RypmfMMQi6lncIk/EPgy0XbS1plKW0tLBgB", - "type": "thinking" - }, - { - "text": "PROMPT_DONE", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 51, - "duration_ms": 0, - "estimated_cost": 0.0023391, - "prompt_cache_creation_tokens": 194, - "prompt_cached_tokens": 2692, - "prompt_tokens": 13, - "tokens": 2950 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "PROMPT_DONE", - "metadata": { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ] -} diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.txt b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.txt deleted file mode 100644 index e90a4892a..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.txt +++ /dev/null @@ -1,1335 +0,0 @@ -span_tree: -├── workflow:instrumentation [task] -│ input: { -│ "metadata": { -│ "scenario": "flue-instrumentation", -│ "testRunId": "" -│ }, -│ "scenario": "flue-instrumentation" -│ } -│ output: { -│ "scenario": "flue-instrumentation", -│ "status": "done" -│ } -│ metadata: { -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ ├── flue.skill [task] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: "SKILL_DONE" -│ │ metadata: { -│ │ "flue.operation": "skill", -│ │ "flue.session": "skill", -│ │ "flue.workflow_name": "instrumentation", -│ │ "provider": "flue", -│ │ "scenario": "flue-instrumentation" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 89, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0041862, -│ │ │ "prompt_cache_creation_tokens": 670, -│ │ │ "prompt_cached_tokens": 1099, -│ │ │ "prompt_tokens": 3, -│ │ │ "tokens": 1861 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": {} -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ │ error: "{\"content\":[{\"type\":\"text\",\"text\":\"ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'\"}],\"details\":{}}" -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 52, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00201795, -│ │ │ "prompt_cache_creation_tokens": 183, -│ │ │ "prompt_cached_tokens": 1769, -│ │ │ "prompt_tokens": 7, -│ │ │ "tokens": 2011 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 54, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0016611, -│ │ │ "prompt_cache_creation_tokens": 66, -│ │ │ "prompt_cached_tokens": 1952, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2078 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents/skills" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 63, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0018459, -│ │ │ "prompt_cache_creation_tokens": 74, -│ │ │ "prompt_cached_tokens": 2018, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2161 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL.md", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL.md", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 68, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0019618500000000002, -│ │ │ "prompt_cache_creation_tokens": 79, -│ │ │ "prompt_cached_tokens": 2092, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2245 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "lines": 7, -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ "type": "text" -│ │ }, -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/e2e-flue-skill.md" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": true, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "skills", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "e2e-flue-skill", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills/e2e-flue-skill" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL.md", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "anthropic-messages", -│ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ "flue.provider": "anthropic", -│ │ "flue.session": "skill", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "claude-sonnet-4-5-20250929", -│ │ "provider": "anthropic" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 8, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00129555, -│ │ "prompt_cache_creation_tokens": 135, -│ │ "prompt_cached_tokens": 2171, -│ │ "prompt_tokens": 6, -│ │ "tokens": 2320 -│ │ } -│ ├── flue.task [task] -│ │ input: "Reply with exactly TASK_DONE and no other text." -│ │ output: "TASK_DONE" -│ │ metadata: { -│ │ "flue.session": "task", -│ │ "provider": "flue" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Reply with exactly TASK_DONE and no other text.", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "TASK_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.input_mode": "full", -│ │ "flue.model": "gpt-4o-mini", -│ │ "flue.provider": "openai", -│ │ "flue.session": "task:task:", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-4o-mini", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 4, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00012299999999999998, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 804, -│ │ "tokens": 808 -│ │ } -│ └── flue.compact [task] -│ input: { -│ "estimatedTokens": 2950, -│ "reason": "manual" -│ } -│ output: { -│ "completed": true -│ } -│ metadata: { -│ "flue.operation": "compact", -│ "flue.session": "main", -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ └── compaction:manual [task] -│ input: { -│ "estimatedTokens": 2950, -│ "reason": "manual" -│ } -│ output: { -│ "messagesAfter": 2, -│ "messagesBefore": 8 -│ } -│ metadata: { -│ "flue.compaction_reason": "manual", -│ "flue.session": "main", -│ "provider": "flue" -│ } -│ metrics: { -│ "duration_ms": 0, -│ "messages_after": 2, -│ "messages_before": 8 -│ } -│ └── flue.turn [llm] -│ input: [ -│ { -│ "content": [ -│ { -│ "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant thinking]: The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.\n\n[Assistant]: I'll complete this instrumented research flow step by step. Starting with Step 1:\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant thinking]: Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".\n\n[Assistant]: Now proceeding to Step 2:\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant thinking]: Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.\n\n[Assistant]: Now proceeding to Step 3:\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", -│ "type": "text" -│ } -│ ], -│ "role": "user" -│ } -│ ] -│ output: { -│ "content": [ -│ { -│ "text": "## Original Request\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\n\n## Early Progress", -│ "textSignature": "{\"v\":1,\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\"}", -│ "type": "text" -│ } -│ ], -│ "role": "assistant" -│ } -│ metadata: { -│ "flue.api": "openai-responses", -│ "flue.model": "gpt-4o-mini", -│ "flue.provider": "openai", -│ "flue.session": "main", -│ "flue.stop_reason": "stop", -│ "flue.turn_purpose": "compaction_prefix", -│ "model": "gpt-4o-mini", -│ "provider": "openai" -│ } -│ metrics: { -│ "completion_tokens": 0, -│ "duration_ms": 0, -│ "estimated_cost": 0, -│ "prompt_cache_creation_tokens": 0, -│ "prompt_cached_tokens": 0, -│ "prompt_tokens": 0, -│ "tokens": 0 -│ } -└── flue.prompt [task] - input: [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ] - output: "PROMPT_DONE" - metadata: { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - } - metrics: { - "duration_ms": 0 - } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - │ "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_mode": "full", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 196, - │ "duration_ms": 0, - │ "estimated_cost": 0.0113025, - │ "prompt_cache_creation_tokens": 2222, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 10, - │ "tokens": 2428 - │ } - ├── tool:lookup [tool] - │ input: { - │ "query": "flue instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "lookup" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "lookup", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - │ "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "lookup" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - │ "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 2:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_message_offset": 1, - │ "flue.input_mode": "delta", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 158, - │ "duration_ms": 0, - │ "estimated_cost": 0.0039606, - │ "prompt_cache_creation_tokens": 236, - │ "prompt_cached_tokens": 2222, - │ "prompt_tokens": 13, - │ "tokens": 2629 - │ } - ├── tool:web_search [tool] - │ input: { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "web_search" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "web_search", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - │ "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 2:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "web_search" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - │ "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 3:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "url": "https://example.test/flue/reasoning-streams" - │ }, - │ "id": "", - │ "name": "summarize_source", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_message_offset": 3, - │ "flue.input_mode": "delta", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 138, - │ "duration_ms": 0, - │ "estimated_cost": 0.0037239000000000005, - │ "prompt_cache_creation_tokens": 234, - │ "prompt_cached_tokens": 2458, - │ "prompt_tokens": 13, - │ "tokens": 2843 - │ } - ├── tool:summarize_source [tool] - │ input: { - │ "url": "https://example.test/flue/reasoning-streams" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "summarize_source" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "summarize_source", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - └── flue.turn [llm] - input: [ - { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ] - output: { - "content": [ - { - "thinking": "Perfect! The summarize_source call has completed successfully. According to the instructions, I should now reply with exactly \"PROMPT_DONE\" and no other text.", - "thinkingSignature": "Eu8CCm4IDhgCKkAn2QOIIlYfvcs6ynsmhCslHDtBsrTA4CfmimK7zyz6+k6dZldV2JBtEBET41It0gFcGEKC0n0mAhaYVuLutpDDMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIM1xfLptxSF8rKzou8GgwRLuVmfiBYOT4CZIsiMAYz8TpvchMAWodP+T9uZ+dJ47MVdNv+p86nZXmT5d5EWlCroiAOFHHzJH2o0UjJQSquAX6wu+jIC8/DXwurGIIf2ToldPuJWF5HGj8kHMnwh777XS1NUFl1irM24rvP7kTAv3Dp8YEwb8t3g1EsicY7j+C5DzgwxVsPYCVOCgdMqQjtsqpL7AX5KQ9+hR1gK1DQjshv4Gze3hw/+C+9iptd3RlDIuwO0cm8NY+sVqeWVh3TtQujQlVt1rW/HfWjclrtT0LZSGwc6RypmfMMQi6lncIk/EPgy0XbS1plKW0tLBgB", - "type": "thinking" - }, - { - "text": "PROMPT_DONE", - "type": "text" - } - ], - "role": "assistant" - } - metadata: { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - } - metrics: { - "completion_tokens": 51, - "duration_ms": 0, - "estimated_cost": 0.0023391, - "prompt_cache_creation_tokens": 194, - "prompt_cached_tokens": 2692, - "prompt_tokens": 13, - "tokens": 2950 - } diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.json b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.json deleted file mode 100644 index dd92abe29..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.json +++ /dev/null @@ -1,1448 +0,0 @@ -{ - "span_tree": [ - { - "name": "workflow:instrumentation", - "type": "task", - "children": [ - { - "name": "flue.skill", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 89, - "duration_ms": 0, - "estimated_cost": 0.0041862, - "prompt_cache_creation_tokens": 670, - "prompt_cached_tokens": 1099, - "prompt_tokens": 3, - "tokens": 1861 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/e2e-flue-skill.md" - }, - "output": { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "details": {} - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - }, - "error": "{\"content\":[{\"type\":\"text\",\"text\":\"ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'\"}],\"details\":{}}" - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 52, - "duration_ms": 0, - "estimated_cost": 0.00201795, - "prompt_cache_creation_tokens": 183, - "prompt_cached_tokens": 1769, - "prompt_tokens": 7, - "tokens": 2011 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents" - }, - "output": { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 54, - "duration_ms": 0, - "estimated_cost": 0.0016611, - "prompt_cache_creation_tokens": 66, - "prompt_cached_tokens": 1952, - "prompt_tokens": 6, - "tokens": 2078 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills" - }, - "output": { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents/skills" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 63, - "duration_ms": 0, - "estimated_cost": 0.0018459, - "prompt_cache_creation_tokens": 74, - "prompt_cached_tokens": 2018, - "prompt_tokens": 6, - "tokens": 2161 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills/e2e-flue-skill" - }, - "output": { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents/skills/e2e-flue-skill" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 68, - "duration_ms": 0, - "estimated_cost": 0.0019618500000000002, - "prompt_cache_creation_tokens": 79, - "prompt_cached_tokens": 2092, - "prompt_tokens": 6, - "tokens": 2245 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "output": { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "details": { - "lines": 7, - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 8, - "duration_ms": 0, - "estimated_cost": 0.00129555, - "prompt_cache_creation_tokens": 135, - "prompt_cached_tokens": 2171, - "prompt_tokens": 6, - "tokens": 2320 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "SKILL_DONE", - "metadata": { - "flue.operation": "skill", - "flue.session": "skill", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Reply with exactly TASK_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "TASK_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "task:task:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 4, - "duration_ms": 0, - "estimated_cost": 0.00012299999999999998, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 804, - "tokens": 808 - } - } - ], - "input": "Reply with exactly TASK_DONE and no other text.", - "output": "TASK_DONE", - "metadata": { - "flue.session": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.compact", - "type": "task", - "children": [ - { - "name": "compaction:manual", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant thinking]: The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.\n\n[Assistant]: I'll complete this instrumented research flow step by step. Starting with Step 1:\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant thinking]: Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".\n\n[Assistant]: Now proceeding to Step 2:\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant thinking]: Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.\n\n[Assistant]: Now proceeding to Step 3:\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "## Original Request\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\n\n## Early Progress", - "textSignature": "{\"v\":1,\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "compaction_prefix", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 0, - "duration_ms": 0, - "estimated_cost": 0, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 0, - "tokens": 0 - } - } - ], - "input": { - "estimatedTokens": 2950, - "reason": "manual" - }, - "output": { - "messagesAfter": 2, - "messagesBefore": 8 - }, - "metadata": { - "flue.compaction_reason": "manual", - "flue.session": "main", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0, - "messages_after": 2, - "messages_before": 8 - } - } - ], - "input": { - "estimatedTokens": 2950, - "reason": "manual" - }, - "output": { - "completed": true - }, - "metadata": { - "flue.operation": "compact", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": { - "metadata": { - "scenario": "flue-instrumentation", - "testRunId": "" - }, - "scenario": "flue-instrumentation" - }, - "output": { - "scenario": "flue-instrumentation", - "status": "done" - }, - "metadata": { - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.prompt", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_mode": "full", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 196, - "duration_ms": 0, - "estimated_cost": 0.0113025, - "prompt_cache_creation_tokens": 2222, - "prompt_cached_tokens": 0, - "prompt_tokens": 10, - "tokens": 2428 - } - }, - { - "name": "tool:lookup", - "type": "tool", - "children": [], - "input": { - "query": "flue instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "details": { - "customTool": "lookup" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "lookup", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "lookup" - } - ], - "output": { - "content": [ - { - "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 2:", - "type": "text" - }, - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 158, - "duration_ms": 0, - "estimated_cost": 0.0039606, - "prompt_cache_creation_tokens": 236, - "prompt_cached_tokens": 2222, - "prompt_tokens": 13, - "tokens": 2629 - } - }, - { - "name": "tool:web_search", - "type": "tool", - "children": [], - "input": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "details": { - "customTool": "web_search" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "web_search", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 2:", - "type": "text" - }, - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "web_search" - } - ], - "output": { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 138, - "duration_ms": 0, - "estimated_cost": 0.0037239000000000005, - "prompt_cache_creation_tokens": 234, - "prompt_cached_tokens": 2458, - "prompt_tokens": 13, - "tokens": 2843 - } - }, - { - "name": "tool:summarize_source", - "type": "tool", - "children": [], - "input": { - "url": "https://example.test/flue/reasoning-streams" - }, - "output": { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "details": { - "customTool": "summarize_source" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "summarize_source", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ], - "output": { - "content": [ - { - "thinking": "Perfect! The summarize_source call has completed successfully. According to the instructions, I should now reply with exactly \"PROMPT_DONE\" and no other text.", - "thinkingSignature": "Eu8CCm4IDhgCKkAn2QOIIlYfvcs6ynsmhCslHDtBsrTA4CfmimK7zyz6+k6dZldV2JBtEBET41It0gFcGEKC0n0mAhaYVuLutpDDMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIM1xfLptxSF8rKzou8GgwRLuVmfiBYOT4CZIsiMAYz8TpvchMAWodP+T9uZ+dJ47MVdNv+p86nZXmT5d5EWlCroiAOFHHzJH2o0UjJQSquAX6wu+jIC8/DXwurGIIf2ToldPuJWF5HGj8kHMnwh777XS1NUFl1irM24rvP7kTAv3Dp8YEwb8t3g1EsicY7j+C5DzgwxVsPYCVOCgdMqQjtsqpL7AX5KQ9+hR1gK1DQjshv4Gze3hw/+C+9iptd3RlDIuwO0cm8NY+sVqeWVh3TtQujQlVt1rW/HfWjclrtT0LZSGwc6RypmfMMQi6lncIk/EPgy0XbS1plKW0tLBgB", - "type": "thinking" - }, - { - "text": "PROMPT_DONE", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 51, - "duration_ms": 0, - "estimated_cost": 0.0023391, - "prompt_cache_creation_tokens": 194, - "prompt_cached_tokens": 2692, - "prompt_tokens": 13, - "tokens": 2950 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "PROMPT_DONE", - "metadata": { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ] -} diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.txt b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.txt deleted file mode 100644 index e90a4892a..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.txt +++ /dev/null @@ -1,1335 +0,0 @@ -span_tree: -├── workflow:instrumentation [task] -│ input: { -│ "metadata": { -│ "scenario": "flue-instrumentation", -│ "testRunId": "" -│ }, -│ "scenario": "flue-instrumentation" -│ } -│ output: { -│ "scenario": "flue-instrumentation", -│ "status": "done" -│ } -│ metadata: { -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ ├── flue.skill [task] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: "SKILL_DONE" -│ │ metadata: { -│ │ "flue.operation": "skill", -│ │ "flue.session": "skill", -│ │ "flue.workflow_name": "instrumentation", -│ │ "provider": "flue", -│ │ "scenario": "flue-instrumentation" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 89, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0041862, -│ │ │ "prompt_cache_creation_tokens": 670, -│ │ │ "prompt_cached_tokens": 1099, -│ │ │ "prompt_tokens": 3, -│ │ │ "tokens": 1861 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": {} -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ │ error: "{\"content\":[{\"type\":\"text\",\"text\":\"ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'\"}],\"details\":{}}" -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 52, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00201795, -│ │ │ "prompt_cache_creation_tokens": 183, -│ │ │ "prompt_cached_tokens": 1769, -│ │ │ "prompt_tokens": 7, -│ │ │ "tokens": 2011 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 54, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0016611, -│ │ │ "prompt_cache_creation_tokens": 66, -│ │ │ "prompt_cached_tokens": 1952, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2078 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents/skills" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 63, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0018459, -│ │ │ "prompt_cache_creation_tokens": 74, -│ │ │ "prompt_cached_tokens": 2018, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2161 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL.md", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL.md", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 68, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0019618500000000002, -│ │ │ "prompt_cache_creation_tokens": 79, -│ │ │ "prompt_cached_tokens": 2092, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2245 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "lines": 7, -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ "type": "text" -│ │ }, -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/e2e-flue-skill.md" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": true, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "skills", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "e2e-flue-skill", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills/e2e-flue-skill" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL.md", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "anthropic-messages", -│ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ "flue.provider": "anthropic", -│ │ "flue.session": "skill", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "claude-sonnet-4-5-20250929", -│ │ "provider": "anthropic" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 8, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00129555, -│ │ "prompt_cache_creation_tokens": 135, -│ │ "prompt_cached_tokens": 2171, -│ │ "prompt_tokens": 6, -│ │ "tokens": 2320 -│ │ } -│ ├── flue.task [task] -│ │ input: "Reply with exactly TASK_DONE and no other text." -│ │ output: "TASK_DONE" -│ │ metadata: { -│ │ "flue.session": "task", -│ │ "provider": "flue" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Reply with exactly TASK_DONE and no other text.", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "TASK_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.input_mode": "full", -│ │ "flue.model": "gpt-4o-mini", -│ │ "flue.provider": "openai", -│ │ "flue.session": "task:task:", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-4o-mini", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 4, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00012299999999999998, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 804, -│ │ "tokens": 808 -│ │ } -│ └── flue.compact [task] -│ input: { -│ "estimatedTokens": 2950, -│ "reason": "manual" -│ } -│ output: { -│ "completed": true -│ } -│ metadata: { -│ "flue.operation": "compact", -│ "flue.session": "main", -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ └── compaction:manual [task] -│ input: { -│ "estimatedTokens": 2950, -│ "reason": "manual" -│ } -│ output: { -│ "messagesAfter": 2, -│ "messagesBefore": 8 -│ } -│ metadata: { -│ "flue.compaction_reason": "manual", -│ "flue.session": "main", -│ "provider": "flue" -│ } -│ metrics: { -│ "duration_ms": 0, -│ "messages_after": 2, -│ "messages_before": 8 -│ } -│ └── flue.turn [llm] -│ input: [ -│ { -│ "content": [ -│ { -│ "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant thinking]: The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.\n\n[Assistant]: I'll complete this instrumented research flow step by step. Starting with Step 1:\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant thinking]: Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".\n\n[Assistant]: Now proceeding to Step 2:\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant thinking]: Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.\n\n[Assistant]: Now proceeding to Step 3:\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", -│ "type": "text" -│ } -│ ], -│ "role": "user" -│ } -│ ] -│ output: { -│ "content": [ -│ { -│ "text": "## Original Request\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\n\n## Early Progress", -│ "textSignature": "{\"v\":1,\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\"}", -│ "type": "text" -│ } -│ ], -│ "role": "assistant" -│ } -│ metadata: { -│ "flue.api": "openai-responses", -│ "flue.model": "gpt-4o-mini", -│ "flue.provider": "openai", -│ "flue.session": "main", -│ "flue.stop_reason": "stop", -│ "flue.turn_purpose": "compaction_prefix", -│ "model": "gpt-4o-mini", -│ "provider": "openai" -│ } -│ metrics: { -│ "completion_tokens": 0, -│ "duration_ms": 0, -│ "estimated_cost": 0, -│ "prompt_cache_creation_tokens": 0, -│ "prompt_cached_tokens": 0, -│ "prompt_tokens": 0, -│ "tokens": 0 -│ } -└── flue.prompt [task] - input: [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ] - output: "PROMPT_DONE" - metadata: { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - } - metrics: { - "duration_ms": 0 - } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - │ "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_mode": "full", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 196, - │ "duration_ms": 0, - │ "estimated_cost": 0.0113025, - │ "prompt_cache_creation_tokens": 2222, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 10, - │ "tokens": 2428 - │ } - ├── tool:lookup [tool] - │ input: { - │ "query": "flue instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "lookup" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "lookup", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - │ "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "lookup" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - │ "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 2:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_message_offset": 1, - │ "flue.input_mode": "delta", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 158, - │ "duration_ms": 0, - │ "estimated_cost": 0.0039606, - │ "prompt_cache_creation_tokens": 236, - │ "prompt_cached_tokens": 2222, - │ "prompt_tokens": 13, - │ "tokens": 2629 - │ } - ├── tool:web_search [tool] - │ input: { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "web_search" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "web_search", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - │ "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 2:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "web_search" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - │ "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 3:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "url": "https://example.test/flue/reasoning-streams" - │ }, - │ "id": "", - │ "name": "summarize_source", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_message_offset": 3, - │ "flue.input_mode": "delta", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 138, - │ "duration_ms": 0, - │ "estimated_cost": 0.0037239000000000005, - │ "prompt_cache_creation_tokens": 234, - │ "prompt_cached_tokens": 2458, - │ "prompt_tokens": 13, - │ "tokens": 2843 - │ } - ├── tool:summarize_source [tool] - │ input: { - │ "url": "https://example.test/flue/reasoning-streams" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "summarize_source" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "summarize_source", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - └── flue.turn [llm] - input: [ - { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ] - output: { - "content": [ - { - "thinking": "Perfect! The summarize_source call has completed successfully. According to the instructions, I should now reply with exactly \"PROMPT_DONE\" and no other text.", - "thinkingSignature": "Eu8CCm4IDhgCKkAn2QOIIlYfvcs6ynsmhCslHDtBsrTA4CfmimK7zyz6+k6dZldV2JBtEBET41It0gFcGEKC0n0mAhaYVuLutpDDMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIM1xfLptxSF8rKzou8GgwRLuVmfiBYOT4CZIsiMAYz8TpvchMAWodP+T9uZ+dJ47MVdNv+p86nZXmT5d5EWlCroiAOFHHzJH2o0UjJQSquAX6wu+jIC8/DXwurGIIf2ToldPuJWF5HGj8kHMnwh777XS1NUFl1irM24rvP7kTAv3Dp8YEwb8t3g1EsicY7j+C5DzgwxVsPYCVOCgdMqQjtsqpL7AX5KQ9+hR1gK1DQjshv4Gze3hw/+C+9iptd3RlDIuwO0cm8NY+sVqeWVh3TtQujQlVt1rW/HfWjclrtT0LZSGwc6RypmfMMQi6lncIk/EPgy0XbS1plKW0tLBgB", - "type": "thinking" - }, - { - "text": "PROMPT_DONE", - "type": "text" - } - ], - "role": "assistant" - } - metadata: { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - } - metrics: { - "completion_tokens": 51, - "duration_ms": 0, - "estimated_cost": 0.0023391, - "prompt_cache_creation_tokens": 194, - "prompt_cached_tokens": 2692, - "prompt_tokens": 13, - "tokens": 2950 - } diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.json b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.json deleted file mode 100644 index dd92abe29..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.json +++ /dev/null @@ -1,1448 +0,0 @@ -{ - "span_tree": [ - { - "name": "workflow:instrumentation", - "type": "task", - "children": [ - { - "name": "flue.skill", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 89, - "duration_ms": 0, - "estimated_cost": 0.0041862, - "prompt_cache_creation_tokens": 670, - "prompt_cached_tokens": 1099, - "prompt_tokens": 3, - "tokens": 1861 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/e2e-flue-skill.md" - }, - "output": { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "details": {} - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - }, - "error": "{\"content\":[{\"type\":\"text\",\"text\":\"ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'\"}],\"details\":{}}" - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 52, - "duration_ms": 0, - "estimated_cost": 0.00201795, - "prompt_cache_creation_tokens": 183, - "prompt_cached_tokens": 1769, - "prompt_tokens": 7, - "tokens": 2011 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents" - }, - "output": { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 54, - "duration_ms": 0, - "estimated_cost": 0.0016611, - "prompt_cache_creation_tokens": 66, - "prompt_cached_tokens": 1952, - "prompt_tokens": 6, - "tokens": 2078 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills" - }, - "output": { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents/skills" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 63, - "duration_ms": 0, - "estimated_cost": 0.0018459, - "prompt_cache_creation_tokens": 74, - "prompt_cached_tokens": 2018, - "prompt_tokens": 6, - "tokens": 2161 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills/e2e-flue-skill" - }, - "output": { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "details": { - "entries": 1, - "isDirectory": true, - "path": ".agents/skills/e2e-flue-skill" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 68, - "duration_ms": 0, - "estimated_cost": 0.0019618500000000002, - "prompt_cache_creation_tokens": 79, - "prompt_cached_tokens": 2092, - "prompt_tokens": 6, - "tokens": 2245 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "output": { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "details": { - "lines": 7, - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", - "type": "text" - }, - { - "arguments": { - "path": ".agents/e2e-flue-skill.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "skills", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - }, - { - "content": [ - { - "arguments": { - "path": ".agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "skill", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 8, - "duration_ms": 0, - "estimated_cost": 0.00129555, - "prompt_cache_creation_tokens": 135, - "prompt_cached_tokens": 2171, - "prompt_tokens": 6, - "tokens": 2320 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "SKILL_DONE", - "metadata": { - "flue.operation": "skill", - "flue.session": "skill", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Reply with exactly TASK_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "TASK_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "task:task:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 4, - "duration_ms": 0, - "estimated_cost": 0.00012299999999999998, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 804, - "tokens": 808 - } - } - ], - "input": "Reply with exactly TASK_DONE and no other text.", - "output": "TASK_DONE", - "metadata": { - "flue.session": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.compact", - "type": "task", - "children": [ - { - "name": "compaction:manual", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant thinking]: The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.\n\n[Assistant]: I'll complete this instrumented research flow step by step. Starting with Step 1:\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant thinking]: Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".\n\n[Assistant]: Now proceeding to Step 2:\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant thinking]: Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.\n\n[Assistant]: Now proceeding to Step 3:\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "## Original Request\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\n\n## Early Progress", - "textSignature": "{\"v\":1,\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "compaction_prefix", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 0, - "duration_ms": 0, - "estimated_cost": 0, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 0, - "tokens": 0 - } - } - ], - "input": { - "estimatedTokens": 2950, - "reason": "manual" - }, - "output": { - "messagesAfter": 2, - "messagesBefore": 8 - }, - "metadata": { - "flue.compaction_reason": "manual", - "flue.session": "main", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0, - "messages_after": 2, - "messages_before": 8 - } - } - ], - "input": { - "estimatedTokens": 2950, - "reason": "manual" - }, - "output": { - "completed": true - }, - "metadata": { - "flue.operation": "compact", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": { - "metadata": { - "scenario": "flue-instrumentation", - "testRunId": "" - }, - "scenario": "flue-instrumentation" - }, - "output": { - "scenario": "flue-instrumentation", - "status": "done" - }, - "metadata": { - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.prompt", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_mode": "full", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 196, - "duration_ms": 0, - "estimated_cost": 0.0113025, - "prompt_cache_creation_tokens": 2222, - "prompt_cached_tokens": 0, - "prompt_tokens": 10, - "tokens": 2428 - } - }, - { - "name": "tool:lookup", - "type": "tool", - "children": [], - "input": { - "query": "flue instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "details": { - "customTool": "lookup" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "lookup", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - "type": "thinking" - }, - { - "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - "type": "text" - }, - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "lookup" - } - ], - "output": { - "content": [ - { - "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 2:", - "type": "text" - }, - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 158, - "duration_ms": 0, - "estimated_cost": 0.0039606, - "prompt_cache_creation_tokens": 236, - "prompt_cached_tokens": 2222, - "prompt_tokens": 13, - "tokens": 2629 - } - }, - { - "name": "tool:web_search", - "type": "tool", - "children": [], - "input": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "details": { - "customTool": "web_search" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "web_search", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 2:", - "type": "text" - }, - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "web_search" - } - ], - "output": { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 138, - "duration_ms": 0, - "estimated_cost": 0.0037239000000000005, - "prompt_cache_creation_tokens": 234, - "prompt_cached_tokens": 2458, - "prompt_tokens": 13, - "tokens": 2843 - } - }, - { - "name": "tool:summarize_source", - "type": "tool", - "children": [], - "input": { - "url": "https://example.test/flue/reasoning-streams" - }, - "output": { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "details": { - "customTool": "summarize_source" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "summarize_source", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ], - "output": { - "content": [ - { - "thinking": "Perfect! The summarize_source call has completed successfully. According to the instructions, I should now reply with exactly \"PROMPT_DONE\" and no other text.", - "thinkingSignature": "Eu8CCm4IDhgCKkAn2QOIIlYfvcs6ynsmhCslHDtBsrTA4CfmimK7zyz6+k6dZldV2JBtEBET41It0gFcGEKC0n0mAhaYVuLutpDDMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIM1xfLptxSF8rKzou8GgwRLuVmfiBYOT4CZIsiMAYz8TpvchMAWodP+T9uZ+dJ47MVdNv+p86nZXmT5d5EWlCroiAOFHHzJH2o0UjJQSquAX6wu+jIC8/DXwurGIIf2ToldPuJWF5HGj8kHMnwh777XS1NUFl1irM24rvP7kTAv3Dp8YEwb8t3g1EsicY7j+C5DzgwxVsPYCVOCgdMqQjtsqpL7AX5KQ9+hR1gK1DQjshv4Gze3hw/+C+9iptd3RlDIuwO0cm8NY+sVqeWVh3TtQujQlVt1rW/HfWjclrtT0LZSGwc6RypmfMMQi6lncIk/EPgy0XbS1plKW0tLBgB", - "type": "thinking" - }, - { - "text": "PROMPT_DONE", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - }, - "metrics": { - "completion_tokens": 51, - "duration_ms": 0, - "estimated_cost": 0.0023391, - "prompt_cache_creation_tokens": 194, - "prompt_cached_tokens": 2692, - "prompt_tokens": 13, - "tokens": 2950 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "PROMPT_DONE", - "metadata": { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ] -} diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.txt b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.txt deleted file mode 100644 index e90a4892a..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.txt +++ /dev/null @@ -1,1335 +0,0 @@ -span_tree: -├── workflow:instrumentation [task] -│ input: { -│ "metadata": { -│ "scenario": "flue-instrumentation", -│ "testRunId": "" -│ }, -│ "scenario": "flue-instrumentation" -│ } -│ output: { -│ "scenario": "flue-instrumentation", -│ "status": "done" -│ } -│ metadata: { -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ ├── flue.skill [task] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: "SKILL_DONE" -│ │ metadata: { -│ │ "flue.operation": "skill", -│ │ "flue.session": "skill", -│ │ "flue.workflow_name": "instrumentation", -│ │ "provider": "flue", -│ │ "scenario": "flue-instrumentation" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 89, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0041862, -│ │ │ "prompt_cache_creation_tokens": 670, -│ │ │ "prompt_cached_tokens": 1099, -│ │ │ "prompt_tokens": 3, -│ │ │ "tokens": 1861 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": {} -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ │ error: "{\"content\":[{\"type\":\"text\",\"text\":\"ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'\"}],\"details\":{}}" -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 52, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00201795, -│ │ │ "prompt_cache_creation_tokens": 183, -│ │ │ "prompt_cached_tokens": 1769, -│ │ │ "prompt_tokens": 7, -│ │ │ "tokens": 2011 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 54, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0016611, -│ │ │ "prompt_cache_creation_tokens": 66, -│ │ │ "prompt_cached_tokens": 1952, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2078 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents/skills" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 63, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0018459, -│ │ │ "prompt_cache_creation_tokens": 74, -│ │ │ "prompt_cached_tokens": 2018, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2161 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL.md", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "entries": 1, -│ │ │ "isDirectory": true, -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ │ "type": "text" -│ │ │ }, -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/e2e-flue-skill.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": true, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "skills", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "e2e-flue-skill", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL.md", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "read" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "read", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "anthropic-messages", -│ │ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ │ "flue.provider": "anthropic", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "claude-sonnet-4-5-20250929", -│ │ │ "provider": "anthropic" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 68, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.0019618500000000002, -│ │ │ "prompt_cache_creation_tokens": 79, -│ │ │ "prompt_cached_tokens": 2092, -│ │ │ "prompt_tokens": 6, -│ │ │ "tokens": 2245 -│ │ │ } -│ │ ├── tool:read [tool] -│ │ │ input: { -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "lines": 7, -│ │ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "read", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "I'll run the e2e-flue-skill. Let me first read the skill file to understand its instructions.", -│ │ "type": "text" -│ │ }, -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/e2e-flue-skill.md" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "ENOENT: no such file or directory, open '/e2e/scenarios/flue-instrumentation/.agents/e2e-flue-skill.md'", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": true, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "skills", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "e2e-flue-skill", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills/e2e-flue-skill" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL.md", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "path": ".agents/skills/e2e-flue-skill/SKILL.md" -│ │ }, -│ │ "id": "", -│ │ "name": "read", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "read" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "anthropic-messages", -│ │ "flue.model": "claude-sonnet-4-5-20250929", -│ │ "flue.provider": "anthropic", -│ │ "flue.session": "skill", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "claude-sonnet-4-5-20250929", -│ │ "provider": "anthropic" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 8, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00129555, -│ │ "prompt_cache_creation_tokens": 135, -│ │ "prompt_cached_tokens": 2171, -│ │ "prompt_tokens": 6, -│ │ "tokens": 2320 -│ │ } -│ ├── flue.task [task] -│ │ input: "Reply with exactly TASK_DONE and no other text." -│ │ output: "TASK_DONE" -│ │ metadata: { -│ │ "flue.session": "task", -│ │ "provider": "flue" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Reply with exactly TASK_DONE and no other text.", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "TASK_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_09b32b2821e9a4d8016a184600019081929d34bb7d4c6a246f\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.input_mode": "full", -│ │ "flue.model": "gpt-4o-mini", -│ │ "flue.provider": "openai", -│ │ "flue.session": "task:task:", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-4o-mini", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 4, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00012299999999999998, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 804, -│ │ "tokens": 808 -│ │ } -│ └── flue.compact [task] -│ input: { -│ "estimatedTokens": 2950, -│ "reason": "manual" -│ } -│ output: { -│ "completed": true -│ } -│ metadata: { -│ "flue.operation": "compact", -│ "flue.session": "main", -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ └── compaction:manual [task] -│ input: { -│ "estimatedTokens": 2950, -│ "reason": "manual" -│ } -│ output: { -│ "messagesAfter": 2, -│ "messagesBefore": 8 -│ } -│ metadata: { -│ "flue.compaction_reason": "manual", -│ "flue.session": "main", -│ "provider": "flue" -│ } -│ metrics: { -│ "duration_ms": 0, -│ "messages_after": 2, -│ "messages_before": 8 -│ } -│ └── flue.turn [llm] -│ input: [ -│ { -│ "content": [ -│ { -│ "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant thinking]: The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.\n\n[Assistant]: I'll complete this instrumented research flow step by step. Starting with Step 1:\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant thinking]: Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".\n\n[Assistant]: Now proceeding to Step 2:\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant thinking]: Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.\n\n[Assistant]: Now proceeding to Step 3:\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", -│ "type": "text" -│ } -│ ], -│ "role": "user" -│ } -│ ] -│ output: { -│ "content": [ -│ { -│ "text": "## Original Request\nThe user requested to complete an instrumented research flow consisting of three specific steps, involving calling various tools in a sequential manner.\n\n## Early Progress", -│ "textSignature": "{\"v\":1,\"id\":\"msg_01ecf68325f9e202016a184601309081a0878ef4054e950d32\"}", -│ "type": "text" -│ } -│ ], -│ "role": "assistant" -│ } -│ metadata: { -│ "flue.api": "openai-responses", -│ "flue.model": "gpt-4o-mini", -│ "flue.provider": "openai", -│ "flue.session": "main", -│ "flue.stop_reason": "stop", -│ "flue.turn_purpose": "compaction_prefix", -│ "model": "gpt-4o-mini", -│ "provider": "openai" -│ } -│ metrics: { -│ "completion_tokens": 0, -│ "duration_ms": 0, -│ "estimated_cost": 0, -│ "prompt_cache_creation_tokens": 0, -│ "prompt_cached_tokens": 0, -│ "prompt_tokens": 0, -│ "tokens": 0 -│ } -└── flue.prompt [task] - input: [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ] - output: "PROMPT_DONE" - metadata: { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - } - metrics: { - "duration_ms": 0 - } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - │ "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_mode": "full", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 196, - │ "duration_ms": 0, - │ "estimated_cost": 0.0113025, - │ "prompt_cache_creation_tokens": 2222, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 10, - │ "tokens": 2428 - │ } - ├── tool:lookup [tool] - │ input: { - │ "query": "flue instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "lookup" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "lookup", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "thinking": "The user wants me to complete a research flow with exactly three steps:\n1. Call lookup with query \"flue instrumentation\"\n2. Call web_search with the lookupId from step 1 and query \"Braintrust Flue reasoning stream instrumentation\"\n3. Call summarize_source with the first URL from step 2\n4. Reply with exactly \"PROMPT_DONE\"\n\nThe user emphasizes I should call exactly one tool per turn and wait for each result. Let me start with step 1.", - │ "thinkingSignature": "EoQFCm4IDhgCKkD+AoKMhpXNPUJ5eANwM28uUCJPCE+AXh0ACw7aXniX+YN04zdJl3hlHMbtAuKhhczLiEeOJtu4KtVpea5u5NFdMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMIOTWCNEtsiI9vF4SGgwPXC861uZx8lpOziEiMLsCukGIluD0U44degzYnHFc+BwXNIEB+yatSHlZE+a5smr9crZcft3OLmsIdXuaFSrDAyQxWd9oqfJdnvza9eWIaOCeNkVCjrTc/wotBwh5Xp8m9k/WKt7P4zzUJBuLQ1f6ZnsdWkL8OiL/2E7Up55z2+UuVWsg1Y90/x27YRFdQjv3RnztYp4bZ5oIHAlTJoCzWiBYEh/Z8c2O+wy6MHo1cp0T57OroB/4HAqRZxrjTOXpKYLmYKXAY5tU8DTd3A0/FfEhg3oPSbOhX+112szFCpm3BRNJYooUUhwSu1hO4H/6r7d/Yp9XIM4tBfzuDx83CC/YetdOFgnYkdliw0xaUGaj4o37rWIRctWHAMdlUc4NDA0Ljc1uirHDXkN8EJwWSoIjPSBTA/p/wgLrvRV5Yy9Z0dMoWOpgYF6t1H/tNAyY8ml1CA7IWyd3jnfTZ1/G3Y0xFDwJDFlb4/QKc67w36SSCa+X9mMoTxD/DYN249lsII+XX3lQ7F1zJ07THOtCyRirCaRhmE/kGwl5tiB1QHrWa9bNHxQxf9i92435JGKN053Fe14AR70MtBSzIU2/zbgvhB1vF5lZt2i46hNxfo4lXR495gEwdwC6+8HJnlEJAJkdeHfZKj+RiwXoiYKPKsSbHi83X8Hh32IifuglNxdlxVQYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "I'll complete this instrumented research flow step by step. Starting with Step 1:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "lookup" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - │ "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 2:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_message_offset": 1, - │ "flue.input_mode": "delta", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 158, - │ "duration_ms": 0, - │ "estimated_cost": 0.0039606, - │ "prompt_cache_creation_tokens": 236, - │ "prompt_cached_tokens": 2222, - │ "prompt_tokens": 13, - │ "tokens": 2629 - │ } - ├── tool:web_search [tool] - │ input: { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "web_search" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "web_search", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "thinking": "Good, I got the lookup result with id \"flue-session-2026\". Now I need to proceed to step 2: call web_search with this lookupId and the query \"Braintrust Flue reasoning stream instrumentation\".", - │ "thinkingSignature": "EpEDCm4IDhgCKkAMahve1kB1j4njp1ZSzO8eCK4bb5Ex7MB6tjdoOXfuUDRfkBqvIGdwVly8GiA8GGYC7Y/RVfREXF6A6OXHTwFyMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMLw+fsa4yo7a7ZSoIGgyaTdvH020n2dQj9bUiMB4OokY29w+uZb4rqDnKDbLXUqUj6U0f1QCGQF2ZBYDDfDJyM5Jv1xY3rQiUUjkhBirQAS4XivjvMnWI5zHBmpI/u/pq4aGdrdb3vUsJ/ne6R1d80JA/INGF8vLkQcZ5xxEK0jU4yRE+ldc2uzavmtoxIKAFD6bqKks1Qv6u+TIZtuW7tUB/7jFxDw93KmJNruMIa8c1J6pPUyBy3xE7QwgmU0kDfIfdnljxLoEBiMwtnxjCPj9lT7HHz4FkrhbnQTV4MvAQohCLjaSbOTd6LdLxwtJPm9mc26ID4eYN1gCCzyAxGXhqLHlzc23PafvQjsZliWnLhaiNMnG0YEo78XkuJQsYAQ==", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 2:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "web_search" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - │ "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - │ "type": "thinking" - │ }, - │ { - │ "text": "Now proceeding to Step 3:", - │ "type": "text" - │ }, - │ { - │ "arguments": { - │ "url": "https://example.test/flue/reasoning-streams" - │ }, - │ "id": "", - │ "name": "summarize_source", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "anthropic-messages", - │ "flue.input_message_offset": 3, - │ "flue.input_mode": "delta", - │ "flue.model": "claude-sonnet-4-5-20250929", - │ "flue.provider": "anthropic", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "claude-sonnet-4-5-20250929", - │ "provider": "anthropic" - │ } - │ metrics: { - │ "completion_tokens": 138, - │ "duration_ms": 0, - │ "estimated_cost": 0.0037239000000000005, - │ "prompt_cache_creation_tokens": 234, - │ "prompt_cached_tokens": 2458, - │ "prompt_tokens": 13, - │ "tokens": 2843 - │ } - ├── tool:summarize_source [tool] - │ input: { - │ "url": "https://example.test/flue/reasoning-streams" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "summarize_source" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "summarize_source", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - └── flue.turn [llm] - input: [ - { - "content": [ - { - "thinking": "Perfect! I got the web_search results with one result. The first (and only) result has the URL \"https://example.test/flue/reasoning-streams\". Now I need to proceed to step 3: call summarize_source with this URL.", - "thinkingSignature": "EqQDCm4IDhgCKkAXv7EDAhWMcrBz11bUd5qxlzyy03CAGQfRgLHDE0uKzLATh1o2sedd8GxLwwQqV0CmLRfNxsdK/ZDp3Lks9iysMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIMXDMniOy5Dy0KI8x7GgzgHs7BYJMs6/N0ur8iMPe0SwnHhP39OjEu7P8ASwK9ffwa3VP67QOlPzOstRRdEZNd0mq+BVjpmBXB+QcAzCrjAeAh/UO/d7SfFmXjqwA3//thoL/TTlGa6W+fdOCFRikn65JWT4ZdVFetfqD+9/QCP/nXEwMsLnTBAxC4EGryrgs2YT+OWwmEJ7rHKFeDs5djhwSb2J2ZJO1kRvD9s2vCCDkImygYBSIVgx1Y0CZJHhzn2Vf39v0Fbth2wDZa+OqxivV9NmDZEjmgcM+tTRx1CCnaww8LZEJQ68v/dUJy7hhD4MrNcQ62evJYPfT3+4EKKWJ/7HI7KXz91//+YAkph63XstbJMFJ3AldAFiq3ecauXt5r42N5gmCmwZX+sI48ENLGGAE=", - "type": "thinking" - }, - { - "text": "Now proceeding to Step 3:", - "type": "text" - }, - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ] - output: { - "content": [ - { - "thinking": "Perfect! The summarize_source call has completed successfully. According to the instructions, I should now reply with exactly \"PROMPT_DONE\" and no other text.", - "thinkingSignature": "Eu8CCm4IDhgCKkAn2QOIIlYfvcs6ynsmhCslHDtBsrTA4CfmimK7zyz6+k6dZldV2JBtEBET41It0gFcGEKC0n0mAhaYVuLutpDDMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAQgh0aGlua2luZxIM1xfLptxSF8rKzou8GgwRLuVmfiBYOT4CZIsiMAYz8TpvchMAWodP+T9uZ+dJ47MVdNv+p86nZXmT5d5EWlCroiAOFHHzJH2o0UjJQSquAX6wu+jIC8/DXwurGIIf2ToldPuJWF5HGj8kHMnwh777XS1NUFl1irM24rvP7kTAv3Dp8YEwb8t3g1EsicY7j+C5DzgwxVsPYCVOCgdMqQjtsqpL7AX5KQ9+hR1gK1DQjshv4Gze3hw/+C+9iptd3RlDIuwO0cm8NY+sVqeWVh3TtQujQlVt1rW/HfWjclrtT0LZSGwc6RypmfMMQi6lncIk/EPgy0XbS1plKW0tLBgB", - "type": "thinking" - }, - { - "text": "PROMPT_DONE", - "type": "text" - } - ], - "role": "assistant" - } - metadata: { - "flue.api": "anthropic-messages", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "claude-sonnet-4-5-20250929", - "flue.provider": "anthropic", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "claude-sonnet-4-5-20250929", - "provider": "anthropic" - } - metrics: { - "completion_tokens": 51, - "duration_ms": 0, - "estimated_cost": 0.0023391, - "prompt_cache_creation_tokens": 194, - "prompt_cached_tokens": 2692, - "prompt_tokens": 13, - "tokens": 2950 - } diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-auto-hook.span-tree.json b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-auto-hook.span-tree.json deleted file mode 100644 index e26112b02..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-auto-hook.span-tree.json +++ /dev/null @@ -1,1662 +0,0 @@ -{ - "span_tree": [ - { - "name": "workflow:instrumentation", - "type": "task", - "children": [ - { - "name": "flue.skill", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [ - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 113, - "duration_ms": 0, - "estimated_cost": 0.00031225000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 855, - "tokens": 968 - } - }, - { - "name": "tool:task", - "type": "tool", - "children": [], - "input": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "output": { - "content": [ - { - "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "text" - } - ], - "details": {} - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - }, - "error": "{\"content\":[{\"type\":\"text\",\"text\":\"[flue] Subagent \\\"e2e-flue-skill\\\" is not declared. Available: (none).\"}],\"details\":{}}" - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "task" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 94, - "duration_ms": 0, - "estimated_cost": 0.00031790000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1002, - "tokens": 1096 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 87, - "duration_ms": 0, - "estimated_cost": 0.00034815000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1197, - "tokens": 1284 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 58, - "duration_ms": 0, - "estimated_cost": 0.0003321, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1298, - "tokens": 1356 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 7, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 68, - "duration_ms": 0, - "estimated_cost": 0.00035900000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1370, - "tokens": 1438 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 9, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 109, - "duration_ms": 0, - "estimated_cost": 0.00043885000000000007, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1513, - "tokens": 1622 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "text" - } - ], - "details": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 11, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 64, - "duration_ms": 0, - "estimated_cost": 0.00015592, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 226, - "tokens": 1826 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "output": { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "details": { - "lines": 7, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 13, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 90, - "duration_ms": 0, - "estimated_cost": 0.00021242000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 346, - "tokens": 1972 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", - "type": "text" - } - ], - "details": { - "command": "cat ./package.json", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 15, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 59, - "duration_ms": 0, - "estimated_cost": 0.0004814700000000001, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 1885, - "tokens": 3480 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "details": { - "command": "printf SKILL_DONE", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 17, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00031035000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 2560, - "prompt_tokens": 1252, - "tokens": 3819 - } - } - ], - "input": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "output": "SKILL_DONE", - "metadata": { - "flue.session": "skill", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 109, - "duration_ms": 0, - "estimated_cost": 0.00030565000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 847, - "tokens": 956 - } - }, - { - "name": "tool:task", - "type": "tool", - "children": [], - "input": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "details": { - "cwd": ".", - "messageId": "", - "session": "task:skill:", - "taskId": "" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "arguments": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "task" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "skill", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00020255000000000002, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 969, - "tokens": 976 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "SKILL_DONE", - "metadata": { - "flue.operation": "skill", - "flue.session": "skill", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Reply with exactly TASK_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "TASK_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "task:task:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 4, - "duration_ms": 0, - "estimated_cost": 0.0001263, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 826, - "tokens": 830 - } - } - ], - "input": "Reply with exactly TASK_DONE and no other text.", - "output": "TASK_DONE", - "metadata": { - "flue.session": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.compact", - "type": "task", - "children": [ - { - "name": "compaction:manual", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "## Original Request\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \"flue instrumentation\" and \"", - "textSignature": "{\"v\":1,\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "compaction_prefix", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 0, - "duration_ms": 0, - "estimated_cost": 0, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 0, - "tokens": 0 - } - } - ], - "input": { - "estimatedTokens": 1229, - "reason": "manual" - }, - "output": { - "messagesAfter": 2, - "messagesBefore": 8 - }, - "metadata": { - "flue.compaction_reason": "manual", - "flue.session": "main", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0, - "messages_after": 2, - "messages_before": 8 - } - } - ], - "input": { - "estimatedTokens": 1229, - "reason": "manual" - }, - "output": { - "completed": true - }, - "metadata": { - "flue.operation": "compact", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": { - "metadata": { - "scenario": "flue-instrumentation", - "testRunId": "" - }, - "scenario": "flue-instrumentation" - }, - "output": { - "scenario": "flue-instrumentation", - "status": "done" - }, - "metadata": { - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.prompt", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 19, - "duration_ms": 0, - "estimated_cost": 0.00022475000000000001, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1005, - "tokens": 1024 - } - }, - { - "name": "tool:lookup", - "type": "tool", - "children": [], - "input": { - "query": "flue instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "details": { - "customTool": "lookup" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "lookup", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "lookup" - } - ], - "output": { - "content": [ - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 34, - "duration_ms": 0, - "estimated_cost": 0.00025350000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1055, - "tokens": 1089 - } - }, - { - "name": "tool:web_search", - "type": "tool", - "children": [], - "input": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "details": { - "customTool": "web_search" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "web_search", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "web_search" - } - ], - "output": { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 30, - "duration_ms": 0, - "estimated_cost": 0.00026690000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1147, - "tokens": 1177 - } - }, - { - "name": "tool:summarize_source", - "type": "tool", - "children": [], - "input": { - "url": "https://example.test/flue/reasoning-streams" - }, - "output": { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "details": { - "customTool": "summarize_source" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "summarize_source", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ], - "output": { - "content": [ - { - "text": "PROMPT_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00025315000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1222, - "tokens": 1229 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "PROMPT_DONE", - "metadata": { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ] -} diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-auto-hook.span-tree.txt b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-auto-hook.span-tree.txt deleted file mode 100644 index abf057e06..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-auto-hook.span-tree.txt +++ /dev/null @@ -1,1499 +0,0 @@ -span_tree: -├── workflow:instrumentation [task] -│ input: { -│ "metadata": { -│ "scenario": "flue-instrumentation", -│ "testRunId": "" -│ }, -│ "scenario": "flue-instrumentation" -│ } -│ output: { -│ "scenario": "flue-instrumentation", -│ "status": "done" -│ } -│ metadata: { -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ ├── flue.skill [task] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: "SKILL_DONE" -│ │ metadata: { -│ │ "flue.operation": "skill", -│ │ "flue.session": "skill", -│ │ "flue.workflow_name": "instrumentation", -│ │ "provider": "flue", -│ │ "scenario": "flue-instrumentation" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "cwd": ".", -│ │ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "task", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "openai-responses", -│ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ "flue.provider": "openai", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "gpt-5.4-nano", -│ │ │ "provider": "openai" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 109, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00030565000000000003, -│ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ "prompt_cached_tokens": 0, -│ │ │ "prompt_tokens": 847, -│ │ │ "tokens": 956 -│ │ │ } -│ │ │ └── flue.task [task] -│ │ │ input: "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ output: "SKILL_DONE" -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "user" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "task", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_mode": "full", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 113, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00031225000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 855, -│ │ │ │ "tokens": 968 -│ │ │ │ } -│ │ │ ├── tool:task [tool] -│ │ │ │ input: { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": {} -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "task", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ │ error: "{\"content\":[{\"type\":\"text\",\"text\":\"[flue] Subagent \\\"e2e-flue-skill\\\" is not declared. Available: (none).\"}],\"details\":{}}" -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "task", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": true, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "task" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 1, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 94, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00031790000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1002, -│ │ │ │ "tokens": 1096 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 3, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 87, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00034815000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1197, -│ │ │ │ "tokens": 1284 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 5, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 58, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.0003321, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1298, -│ │ │ │ "tokens": 1356 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 7, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 68, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00035900000000000005, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1370, -│ │ │ │ "tokens": 1438 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 9, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 109, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00043885000000000007, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1513, -│ │ │ │ "tokens": 1622 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills/e2e-flue-skill/SKILL.md", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills/e2e-flue-skill/SKILL.md", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "read", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 11, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 64, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00015592, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 226, -│ │ │ │ "tokens": 1826 -│ │ │ │ } -│ │ │ ├── tool:read [tool] -│ │ │ │ input: { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "lines": 7, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "read", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "read", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "read" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 13, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 90, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00021242000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 346, -│ │ │ │ "tokens": 1972 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 15, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 59, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.0004814700000000001, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 1885, -│ │ │ │ "tokens": 3480 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "SKILL_DONE", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ └── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "command": "printf SKILL_DONE", -│ │ │ "timeout": 100000 -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "bash", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "bash" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "textSignature": "{\"v\":1,\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"phase\":\"final_answer\"}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "openai-responses", -│ │ │ "flue.input_message_offset": 17, -│ │ │ "flue.input_mode": "delta", -│ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ "flue.provider": "openai", -│ │ │ "flue.session": "task:skill:", -│ │ │ "flue.stop_reason": "stop", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "gpt-5.4-nano", -│ │ │ "provider": "openai" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 7, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00031035000000000004, -│ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ "prompt_cached_tokens": 2560, -│ │ │ "prompt_tokens": 1252, -│ │ │ "tokens": 3819 -│ │ │ } -│ │ ├── tool:task [tool] -│ │ │ input: { -│ │ │ "cwd": ".", -│ │ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "cwd": ".", -│ │ │ "messageId": "", -│ │ │ "session": "task:skill:", -│ │ │ "taskId": "" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "task", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "cwd": ".", -│ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ }, -│ │ "id": "", -│ │ "name": "task", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "task" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"phase\":\"final_answer\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.model": "gpt-5.4-nano", -│ │ "flue.provider": "openai", -│ │ "flue.session": "skill", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-5.4-nano", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 7, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00020255000000000002, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 969, -│ │ "tokens": 976 -│ │ } -│ ├── flue.task [task] -│ │ input: "Reply with exactly TASK_DONE and no other text." -│ │ output: "TASK_DONE" -│ │ metadata: { -│ │ "flue.session": "task", -│ │ "provider": "flue" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Reply with exactly TASK_DONE and no other text.", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "TASK_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.input_mode": "full", -│ │ "flue.model": "gpt-4o-mini", -│ │ "flue.provider": "openai", -│ │ "flue.session": "task:task:", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-4o-mini", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 4, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.0001263, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 826, -│ │ "tokens": 830 -│ │ } -│ └── flue.compact [task] -│ input: { -│ "estimatedTokens": 1229, -│ "reason": "manual" -│ } -│ output: { -│ "completed": true -│ } -│ metadata: { -│ "flue.operation": "compact", -│ "flue.session": "main", -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ └── compaction:manual [task] -│ input: { -│ "estimatedTokens": 1229, -│ "reason": "manual" -│ } -│ output: { -│ "messagesAfter": 2, -│ "messagesBefore": 8 -│ } -│ metadata: { -│ "flue.compaction_reason": "manual", -│ "flue.session": "main", -│ "provider": "flue" -│ } -│ metrics: { -│ "duration_ms": 0, -│ "messages_after": 2, -│ "messages_before": 8 -│ } -│ └── flue.turn [llm] -│ input: [ -│ { -│ "content": [ -│ { -│ "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", -│ "type": "text" -│ } -│ ], -│ "role": "user" -│ } -│ ] -│ output: { -│ "content": [ -│ { -│ "text": "## Original Request\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \"flue instrumentation\" and \"", -│ "textSignature": "{\"v\":1,\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\"}", -│ "type": "text" -│ } -│ ], -│ "role": "assistant" -│ } -│ metadata: { -│ "flue.api": "openai-responses", -│ "flue.model": "gpt-4o-mini", -│ "flue.provider": "openai", -│ "flue.session": "main", -│ "flue.stop_reason": "stop", -│ "flue.turn_purpose": "compaction_prefix", -│ "model": "gpt-4o-mini", -│ "provider": "openai" -│ } -│ metrics: { -│ "completion_tokens": 0, -│ "duration_ms": 0, -│ "estimated_cost": 0, -│ "prompt_cache_creation_tokens": 0, -│ "prompt_cached_tokens": 0, -│ "prompt_tokens": 0, -│ "tokens": 0 -│ } -└── flue.prompt [task] - input: [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ] - output: "PROMPT_DONE" - metadata: { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - } - metrics: { - "duration_ms": 0 - } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_mode": "full", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 19, - │ "duration_ms": 0, - │ "estimated_cost": 0.00022475000000000001, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1005, - │ "tokens": 1024 - │ } - ├── tool:lookup [tool] - │ input: { - │ "query": "flue instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "lookup" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "lookup", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "lookup" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_message_offset": 1, - │ "flue.input_mode": "delta", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 34, - │ "duration_ms": 0, - │ "estimated_cost": 0.00025350000000000004, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1055, - │ "tokens": 1089 - │ } - ├── tool:web_search [tool] - │ input: { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "web_search" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "web_search", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "web_search" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "url": "https://example.test/flue/reasoning-streams" - │ }, - │ "id": "", - │ "name": "summarize_source", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_message_offset": 3, - │ "flue.input_mode": "delta", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 30, - │ "duration_ms": 0, - │ "estimated_cost": 0.00026690000000000004, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1147, - │ "tokens": 1177 - │ } - ├── tool:summarize_source [tool] - │ input: { - │ "url": "https://example.test/flue/reasoning-streams" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "summarize_source" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "summarize_source", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - └── flue.turn [llm] - input: [ - { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ] - output: { - "content": [ - { - "text": "PROMPT_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - } - metadata: { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - } - metrics: { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00025315000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1222, - "tokens": 1229 - } diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-cli.span-tree.json b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-cli.span-tree.json deleted file mode 100644 index e26112b02..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-cli.span-tree.json +++ /dev/null @@ -1,1662 +0,0 @@ -{ - "span_tree": [ - { - "name": "workflow:instrumentation", - "type": "task", - "children": [ - { - "name": "flue.skill", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [ - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 113, - "duration_ms": 0, - "estimated_cost": 0.00031225000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 855, - "tokens": 968 - } - }, - { - "name": "tool:task", - "type": "tool", - "children": [], - "input": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "output": { - "content": [ - { - "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "text" - } - ], - "details": {} - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - }, - "error": "{\"content\":[{\"type\":\"text\",\"text\":\"[flue] Subagent \\\"e2e-flue-skill\\\" is not declared. Available: (none).\"}],\"details\":{}}" - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "task" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 94, - "duration_ms": 0, - "estimated_cost": 0.00031790000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1002, - "tokens": 1096 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 87, - "duration_ms": 0, - "estimated_cost": 0.00034815000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1197, - "tokens": 1284 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 58, - "duration_ms": 0, - "estimated_cost": 0.0003321, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1298, - "tokens": 1356 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 7, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 68, - "duration_ms": 0, - "estimated_cost": 0.00035900000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1370, - "tokens": 1438 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 9, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 109, - "duration_ms": 0, - "estimated_cost": 0.00043885000000000007, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1513, - "tokens": 1622 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "text" - } - ], - "details": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 11, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 64, - "duration_ms": 0, - "estimated_cost": 0.00015592, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 226, - "tokens": 1826 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "output": { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "details": { - "lines": 7, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 13, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 90, - "duration_ms": 0, - "estimated_cost": 0.00021242000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 346, - "tokens": 1972 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", - "type": "text" - } - ], - "details": { - "command": "cat ./package.json", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 15, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 59, - "duration_ms": 0, - "estimated_cost": 0.0004814700000000001, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 1885, - "tokens": 3480 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "details": { - "command": "printf SKILL_DONE", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 17, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00031035000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 2560, - "prompt_tokens": 1252, - "tokens": 3819 - } - } - ], - "input": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "output": "SKILL_DONE", - "metadata": { - "flue.session": "skill", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 109, - "duration_ms": 0, - "estimated_cost": 0.00030565000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 847, - "tokens": 956 - } - }, - { - "name": "tool:task", - "type": "tool", - "children": [], - "input": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "details": { - "cwd": ".", - "messageId": "", - "session": "task:skill:", - "taskId": "" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "arguments": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "task" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "skill", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00020255000000000002, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 969, - "tokens": 976 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "SKILL_DONE", - "metadata": { - "flue.operation": "skill", - "flue.session": "skill", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Reply with exactly TASK_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "TASK_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "task:task:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 4, - "duration_ms": 0, - "estimated_cost": 0.0001263, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 826, - "tokens": 830 - } - } - ], - "input": "Reply with exactly TASK_DONE and no other text.", - "output": "TASK_DONE", - "metadata": { - "flue.session": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.compact", - "type": "task", - "children": [ - { - "name": "compaction:manual", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "## Original Request\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \"flue instrumentation\" and \"", - "textSignature": "{\"v\":1,\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "compaction_prefix", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 0, - "duration_ms": 0, - "estimated_cost": 0, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 0, - "tokens": 0 - } - } - ], - "input": { - "estimatedTokens": 1229, - "reason": "manual" - }, - "output": { - "messagesAfter": 2, - "messagesBefore": 8 - }, - "metadata": { - "flue.compaction_reason": "manual", - "flue.session": "main", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0, - "messages_after": 2, - "messages_before": 8 - } - } - ], - "input": { - "estimatedTokens": 1229, - "reason": "manual" - }, - "output": { - "completed": true - }, - "metadata": { - "flue.operation": "compact", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": { - "metadata": { - "scenario": "flue-instrumentation", - "testRunId": "" - }, - "scenario": "flue-instrumentation" - }, - "output": { - "scenario": "flue-instrumentation", - "status": "done" - }, - "metadata": { - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.prompt", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 19, - "duration_ms": 0, - "estimated_cost": 0.00022475000000000001, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1005, - "tokens": 1024 - } - }, - { - "name": "tool:lookup", - "type": "tool", - "children": [], - "input": { - "query": "flue instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "details": { - "customTool": "lookup" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "lookup", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "lookup" - } - ], - "output": { - "content": [ - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 34, - "duration_ms": 0, - "estimated_cost": 0.00025350000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1055, - "tokens": 1089 - } - }, - { - "name": "tool:web_search", - "type": "tool", - "children": [], - "input": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "details": { - "customTool": "web_search" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "web_search", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "web_search" - } - ], - "output": { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 30, - "duration_ms": 0, - "estimated_cost": 0.00026690000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1147, - "tokens": 1177 - } - }, - { - "name": "tool:summarize_source", - "type": "tool", - "children": [], - "input": { - "url": "https://example.test/flue/reasoning-streams" - }, - "output": { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "details": { - "customTool": "summarize_source" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "summarize_source", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ], - "output": { - "content": [ - { - "text": "PROMPT_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00025315000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1222, - "tokens": 1229 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "PROMPT_DONE", - "metadata": { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ] -} diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-cli.span-tree.txt b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-cli.span-tree.txt deleted file mode 100644 index abf057e06..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-cli.span-tree.txt +++ /dev/null @@ -1,1499 +0,0 @@ -span_tree: -├── workflow:instrumentation [task] -│ input: { -│ "metadata": { -│ "scenario": "flue-instrumentation", -│ "testRunId": "" -│ }, -│ "scenario": "flue-instrumentation" -│ } -│ output: { -│ "scenario": "flue-instrumentation", -│ "status": "done" -│ } -│ metadata: { -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ ├── flue.skill [task] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: "SKILL_DONE" -│ │ metadata: { -│ │ "flue.operation": "skill", -│ │ "flue.session": "skill", -│ │ "flue.workflow_name": "instrumentation", -│ │ "provider": "flue", -│ │ "scenario": "flue-instrumentation" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "cwd": ".", -│ │ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "task", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "openai-responses", -│ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ "flue.provider": "openai", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "gpt-5.4-nano", -│ │ │ "provider": "openai" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 109, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00030565000000000003, -│ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ "prompt_cached_tokens": 0, -│ │ │ "prompt_tokens": 847, -│ │ │ "tokens": 956 -│ │ │ } -│ │ │ └── flue.task [task] -│ │ │ input: "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ output: "SKILL_DONE" -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "user" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "task", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_mode": "full", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 113, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00031225000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 855, -│ │ │ │ "tokens": 968 -│ │ │ │ } -│ │ │ ├── tool:task [tool] -│ │ │ │ input: { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": {} -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "task", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ │ error: "{\"content\":[{\"type\":\"text\",\"text\":\"[flue] Subagent \\\"e2e-flue-skill\\\" is not declared. Available: (none).\"}],\"details\":{}}" -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "task", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": true, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "task" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 1, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 94, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00031790000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1002, -│ │ │ │ "tokens": 1096 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 3, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 87, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00034815000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1197, -│ │ │ │ "tokens": 1284 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 5, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 58, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.0003321, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1298, -│ │ │ │ "tokens": 1356 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 7, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 68, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00035900000000000005, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1370, -│ │ │ │ "tokens": 1438 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 9, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 109, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00043885000000000007, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1513, -│ │ │ │ "tokens": 1622 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills/e2e-flue-skill/SKILL.md", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills/e2e-flue-skill/SKILL.md", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "read", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 11, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 64, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00015592, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 226, -│ │ │ │ "tokens": 1826 -│ │ │ │ } -│ │ │ ├── tool:read [tool] -│ │ │ │ input: { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "lines": 7, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "read", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "read", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "read" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 13, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 90, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00021242000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 346, -│ │ │ │ "tokens": 1972 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 15, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 59, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.0004814700000000001, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 1885, -│ │ │ │ "tokens": 3480 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "SKILL_DONE", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ └── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "command": "printf SKILL_DONE", -│ │ │ "timeout": 100000 -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "bash", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "bash" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "textSignature": "{\"v\":1,\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"phase\":\"final_answer\"}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "openai-responses", -│ │ │ "flue.input_message_offset": 17, -│ │ │ "flue.input_mode": "delta", -│ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ "flue.provider": "openai", -│ │ │ "flue.session": "task:skill:", -│ │ │ "flue.stop_reason": "stop", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "gpt-5.4-nano", -│ │ │ "provider": "openai" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 7, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00031035000000000004, -│ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ "prompt_cached_tokens": 2560, -│ │ │ "prompt_tokens": 1252, -│ │ │ "tokens": 3819 -│ │ │ } -│ │ ├── tool:task [tool] -│ │ │ input: { -│ │ │ "cwd": ".", -│ │ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "cwd": ".", -│ │ │ "messageId": "", -│ │ │ "session": "task:skill:", -│ │ │ "taskId": "" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "task", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "cwd": ".", -│ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ }, -│ │ "id": "", -│ │ "name": "task", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "task" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"phase\":\"final_answer\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.model": "gpt-5.4-nano", -│ │ "flue.provider": "openai", -│ │ "flue.session": "skill", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-5.4-nano", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 7, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00020255000000000002, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 969, -│ │ "tokens": 976 -│ │ } -│ ├── flue.task [task] -│ │ input: "Reply with exactly TASK_DONE and no other text." -│ │ output: "TASK_DONE" -│ │ metadata: { -│ │ "flue.session": "task", -│ │ "provider": "flue" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Reply with exactly TASK_DONE and no other text.", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "TASK_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.input_mode": "full", -│ │ "flue.model": "gpt-4o-mini", -│ │ "flue.provider": "openai", -│ │ "flue.session": "task:task:", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-4o-mini", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 4, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.0001263, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 826, -│ │ "tokens": 830 -│ │ } -│ └── flue.compact [task] -│ input: { -│ "estimatedTokens": 1229, -│ "reason": "manual" -│ } -│ output: { -│ "completed": true -│ } -│ metadata: { -│ "flue.operation": "compact", -│ "flue.session": "main", -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ └── compaction:manual [task] -│ input: { -│ "estimatedTokens": 1229, -│ "reason": "manual" -│ } -│ output: { -│ "messagesAfter": 2, -│ "messagesBefore": 8 -│ } -│ metadata: { -│ "flue.compaction_reason": "manual", -│ "flue.session": "main", -│ "provider": "flue" -│ } -│ metrics: { -│ "duration_ms": 0, -│ "messages_after": 2, -│ "messages_before": 8 -│ } -│ └── flue.turn [llm] -│ input: [ -│ { -│ "content": [ -│ { -│ "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", -│ "type": "text" -│ } -│ ], -│ "role": "user" -│ } -│ ] -│ output: { -│ "content": [ -│ { -│ "text": "## Original Request\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \"flue instrumentation\" and \"", -│ "textSignature": "{\"v\":1,\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\"}", -│ "type": "text" -│ } -│ ], -│ "role": "assistant" -│ } -│ metadata: { -│ "flue.api": "openai-responses", -│ "flue.model": "gpt-4o-mini", -│ "flue.provider": "openai", -│ "flue.session": "main", -│ "flue.stop_reason": "stop", -│ "flue.turn_purpose": "compaction_prefix", -│ "model": "gpt-4o-mini", -│ "provider": "openai" -│ } -│ metrics: { -│ "completion_tokens": 0, -│ "duration_ms": 0, -│ "estimated_cost": 0, -│ "prompt_cache_creation_tokens": 0, -│ "prompt_cached_tokens": 0, -│ "prompt_tokens": 0, -│ "tokens": 0 -│ } -└── flue.prompt [task] - input: [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ] - output: "PROMPT_DONE" - metadata: { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - } - metrics: { - "duration_ms": 0 - } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_mode": "full", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 19, - │ "duration_ms": 0, - │ "estimated_cost": 0.00022475000000000001, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1005, - │ "tokens": 1024 - │ } - ├── tool:lookup [tool] - │ input: { - │ "query": "flue instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "lookup" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "lookup", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "lookup" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_message_offset": 1, - │ "flue.input_mode": "delta", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 34, - │ "duration_ms": 0, - │ "estimated_cost": 0.00025350000000000004, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1055, - │ "tokens": 1089 - │ } - ├── tool:web_search [tool] - │ input: { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "web_search" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "web_search", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "web_search" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "url": "https://example.test/flue/reasoning-streams" - │ }, - │ "id": "", - │ "name": "summarize_source", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_message_offset": 3, - │ "flue.input_mode": "delta", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 30, - │ "duration_ms": 0, - │ "estimated_cost": 0.00026690000000000004, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1147, - │ "tokens": 1177 - │ } - ├── tool:summarize_source [tool] - │ input: { - │ "url": "https://example.test/flue/reasoning-streams" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "summarize_source" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "summarize_source", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - └── flue.turn [llm] - input: [ - { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ] - output: { - "content": [ - { - "text": "PROMPT_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - } - metadata: { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - } - metrics: { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00025315000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1222, - "tokens": 1229 - } diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-explicit.span-tree.json b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-explicit.span-tree.json deleted file mode 100644 index e26112b02..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-explicit.span-tree.json +++ /dev/null @@ -1,1662 +0,0 @@ -{ - "span_tree": [ - { - "name": "workflow:instrumentation", - "type": "task", - "children": [ - { - "name": "flue.skill", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [ - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 113, - "duration_ms": 0, - "estimated_cost": 0.00031225000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 855, - "tokens": 968 - } - }, - { - "name": "tool:task", - "type": "tool", - "children": [], - "input": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "output": { - "content": [ - { - "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "text" - } - ], - "details": {} - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - }, - "error": "{\"content\":[{\"type\":\"text\",\"text\":\"[flue] Subagent \\\"e2e-flue-skill\\\" is not declared. Available: (none).\"}],\"details\":{}}" - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "agent": "e2e-flue-skill", - "cwd": ".", - "description": "Run e2e-flue-skill with marker SKILL_DONE", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", - "type": "text" - } - ], - "isError": true, - "role": "toolResult", - "toolCallId": "", - "toolName": "task" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 94, - "duration_ms": 0, - "estimated_cost": 0.00031790000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1002, - "tokens": 1096 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -name AGENTS.md -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 87, - "duration_ms": 0, - "estimated_cost": 0.00034815000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1197, - "tokens": 1284 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 58, - "duration_ms": 0, - "estimated_cost": 0.0003321, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1298, - "tokens": 1356 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "(no output)", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 7, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 68, - "duration_ms": 0, - "estimated_cost": 0.00035900000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1370, - "tokens": 1438 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "text" - } - ], - "details": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 9, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 109, - "duration_ms": 0, - "estimated_cost": 0.00043885000000000007, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1513, - "tokens": 1622 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "text" - } - ], - "details": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "./.agents/skills/e2e-flue-skill/SKILL.md", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 11, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 64, - "duration_ms": 0, - "estimated_cost": 0.00015592, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 226, - "tokens": 1826 - } - }, - { - "name": "tool:read", - "type": "tool", - "children": [], - "input": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "output": { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "details": { - "lines": 7, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "read", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "limit": 200, - "offset": 1, - "path": "./.agents/skills/e2e-flue-skill/SKILL.md" - }, - "id": "", - "name": "read", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "read" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 13, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 90, - "duration_ms": 0, - "estimated_cost": 0.00021242000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 346, - "tokens": 1972 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", - "type": "text" - } - ], - "details": { - "command": "cat ./package.json", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "cat ./package.json", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "arguments": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 15, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 59, - "duration_ms": 0, - "estimated_cost": 0.0004814700000000001, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 1536, - "prompt_tokens": 1885, - "tokens": 3480 - } - }, - { - "name": "tool:bash", - "type": "tool", - "children": [], - "input": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "details": { - "command": "printf SKILL_DONE", - "exitCode": 0 - } - }, - "metadata": { - "flue.session": "task:skill:", - "flue.tool_name": "bash", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "command": "printf SKILL_DONE", - "timeout": 100000 - }, - "id": "", - "name": "bash", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "bash" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 17, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "task:skill:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00031035000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 2560, - "prompt_tokens": 1252, - "tokens": 3819 - } - } - ], - "input": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", - "output": "SKILL_DONE", - "metadata": { - "flue.session": "skill", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "skill", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 109, - "duration_ms": 0, - "estimated_cost": 0.00030565000000000003, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 847, - "tokens": 956 - } - }, - { - "name": "tool:task", - "type": "tool", - "children": [], - "input": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "output": { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "details": { - "cwd": ".", - "messageId": "", - "session": "task:skill:", - "taskId": "" - } - }, - "metadata": { - "flue.session": "skill", - "flue.tool_name": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - }, - { - "content": [ - { - "arguments": { - "cwd": ".", - "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", - "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." - }, - "id": "", - "name": "task", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "SKILL_DONE", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "task" - } - ], - "output": { - "content": [ - { - "text": "SKILL_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "skill", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00020255000000000002, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 969, - "tokens": 976 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "SKILL_DONE", - "metadata": { - "flue.operation": "skill", - "flue.session": "skill", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.task", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Reply with exactly TASK_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "TASK_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "task:task:", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 4, - "duration_ms": 0, - "estimated_cost": 0.0001263, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 826, - "tokens": 830 - } - } - ], - "input": "Reply with exactly TASK_DONE and no other text.", - "output": "TASK_DONE", - "metadata": { - "flue.session": "task", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.compact", - "type": "task", - "children": [ - { - "name": "compaction:manual", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "text": "## Original Request\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \"flue instrumentation\" and \"", - "textSignature": "{\"v\":1,\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.model": "gpt-4o-mini", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "compaction_prefix", - "model": "gpt-4o-mini", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 0, - "duration_ms": 0, - "estimated_cost": 0, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 0, - "tokens": 0 - } - } - ], - "input": { - "estimatedTokens": 1229, - "reason": "manual" - }, - "output": { - "messagesAfter": 2, - "messagesBefore": 8 - }, - "metadata": { - "flue.compaction_reason": "manual", - "flue.session": "main", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0, - "messages_after": 2, - "messages_before": 8 - } - } - ], - "input": { - "estimatedTokens": 1229, - "reason": "manual" - }, - "output": { - "completed": true - }, - "metadata": { - "flue.operation": "compact", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ], - "input": { - "metadata": { - "scenario": "flue-instrumentation", - "testRunId": "" - }, - "scenario": "flue-instrumentation" - }, - "output": { - "scenario": "flue-instrumentation", - "status": "done" - }, - "metadata": { - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.prompt", - "type": "task", - "children": [ - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": { - "content": [ - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_mode": "full", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 19, - "duration_ms": 0, - "estimated_cost": 0.00022475000000000001, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1005, - "tokens": 1024 - } - }, - { - "name": "tool:lookup", - "type": "tool", - "children": [], - "input": { - "query": "flue instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "details": { - "customTool": "lookup" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "lookup", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "query": "flue instrumentation" - }, - "id": "", - "name": "lookup", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "lookup" - } - ], - "output": { - "content": [ - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 1, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 34, - "duration_ms": 0, - "estimated_cost": 0.00025350000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1055, - "tokens": 1089 - } - }, - { - "name": "tool:web_search", - "type": "tool", - "children": [], - "input": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "output": { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "details": { - "customTool": "web_search" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "web_search", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "lookupId": "flue-session-2026", - "query": "Braintrust Flue reasoning stream instrumentation" - }, - "id": "", - "name": "web_search", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "web_search" - } - ], - "output": { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 3, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "toolUse", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 30, - "duration_ms": 0, - "estimated_cost": 0.00026690000000000004, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1147, - "tokens": 1177 - } - }, - { - "name": "tool:summarize_source", - "type": "tool", - "children": [], - "input": { - "url": "https://example.test/flue/reasoning-streams" - }, - "output": { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "details": { - "customTool": "summarize_source" - } - }, - "metadata": { - "flue.session": "main", - "flue.tool_name": "summarize_source", - "provider": "flue" - }, - "metrics": { - "duration_ms": 0 - } - }, - { - "name": "flue.turn", - "type": "llm", - "children": [], - "input": [ - { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ], - "output": { - "content": [ - { - "text": "PROMPT_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - }, - "metadata": { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - }, - "metrics": { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00025315000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1222, - "tokens": 1229 - } - } - ], - "input": [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ], - "output": "PROMPT_DONE", - "metadata": { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - }, - "metrics": { - "duration_ms": 0 - } - } - ] -} diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-explicit.span-tree.txt b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-explicit.span-tree.txt deleted file mode 100644 index abf057e06..000000000 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-latest-explicit.span-tree.txt +++ /dev/null @@ -1,1499 +0,0 @@ -span_tree: -├── workflow:instrumentation [task] -│ input: { -│ "metadata": { -│ "scenario": "flue-instrumentation", -│ "testRunId": "" -│ }, -│ "scenario": "flue-instrumentation" -│ } -│ output: { -│ "scenario": "flue-instrumentation", -│ "status": "done" -│ } -│ metadata: { -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ ├── flue.skill [task] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: "SKILL_DONE" -│ │ metadata: { -│ │ "flue.operation": "skill", -│ │ "flue.session": "skill", -│ │ "flue.workflow_name": "instrumentation", -│ │ "provider": "flue", -│ │ "scenario": "flue-instrumentation" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ ├── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "user" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "cwd": ".", -│ │ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "task", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "openai-responses", -│ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ "flue.provider": "openai", -│ │ │ "flue.session": "skill", -│ │ │ "flue.stop_reason": "toolUse", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "gpt-5.4-nano", -│ │ │ "provider": "openai" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 109, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00030565000000000003, -│ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ "prompt_cached_tokens": 0, -│ │ │ "prompt_tokens": 847, -│ │ │ "tokens": 956 -│ │ │ } -│ │ │ └── flue.task [task] -│ │ │ input: "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ output: "SKILL_DONE" -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished.", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "user" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "task", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_mode": "full", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 113, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00031225000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 855, -│ │ │ │ "tokens": 968 -│ │ │ │ } -│ │ │ ├── tool:task [tool] -│ │ │ │ input: { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": {} -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "task", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ │ error: "{\"content\":[{\"type\":\"text\",\"text\":\"[flue] Subagent \\\"e2e-flue-skill\\\" is not declared. Available: (none).\"}],\"details\":{}}" -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "agent": "e2e-flue-skill", -│ │ │ │ "cwd": ".", -│ │ │ │ "description": "Run e2e-flue-skill with marker SKILL_DONE", -│ │ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Ensure completion and report the final output marker only." -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "task", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "[flue] Subagent \"e2e-flue-skill\" is not declared. Available: (none).", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": true, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "task" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 1, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 94, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00031790000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1002, -│ │ │ │ "tokens": 1096 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -name AGENTS.md -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 3, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 87, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00034815000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1197, -│ │ │ │ "tokens": 1284 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 4 -type f -name AGENTS.md -print -exec sed -n '1,200p' {} \\;", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 5, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 58, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.0003321, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1298, -│ │ │ │ "tokens": 1356 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 6 -type f -name 'AGENTS.md' -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "(no output)", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 7, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 68, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00035900000000000005, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1370, -│ │ │ │ "tokens": 1438 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find . -maxdepth 5 -type f -name '*flue*skill*' -o -name '*skill*' | head", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills\n./.agents/skills/e2e-flue-skill", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 9, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 109, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00043885000000000007, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 0, -│ │ │ │ "prompt_tokens": 1513, -│ │ │ │ "tokens": 1622 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills/e2e-flue-skill/SKILL.md", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "find ./.agents/skills/e2e-flue-skill -maxdepth 2 -type f -print", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "./.agents/skills/e2e-flue-skill/SKILL.md", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "read", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 11, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 64, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00015592, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 226, -│ │ │ │ "tokens": 1826 -│ │ │ │ } -│ │ │ ├── tool:read [tool] -│ │ │ │ input: { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "lines": 7, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "read", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "limit": 200, -│ │ │ │ "offset": 1, -│ │ │ │ "path": "./.agents/skills/e2e-flue-skill/SKILL.md" -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "read", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "---\nname: e2e-flue-skill\ndescription: Deterministic skill used by the Braintrust Flue instrumentation e2e test.\n---\n\nReturn the marker from the provided args exactly once. Output no other text.\n", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "read" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 13, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 90, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.00021242000000000003, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 346, -│ │ │ │ "tokens": 1972 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ ├── flue.turn [llm] -│ │ │ │ input: [ -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "cat ./package.json", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ }, -│ │ │ │ { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "{\n \"name\": \"flue-instrumentation-scenario\",\n \"private\": true,\n \"type\": \"module\",\n \"braintrustScenario\": {\n \"bump\": {\n \"dependencies\": {\n \"flue-cli-v0-8-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-cli-v1-latest\": {\n \"package\": \"@flue/cli\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v0-8-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=0.8.0 <0.9.0\"\n },\n \"flue-runtime-v1-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=1.0.0-beta.3 <2.0.0-0\",\n \"allowPrerelease\": true\n },\n \"flue-runtime-v2-latest\": {\n \"package\": \"@flue/runtime\",\n \"range\": \">=2.0.0 <3.0.0\"\n }\n }\n }\n },\n \"dependencies\": {\n \"@flue/cli\": \"0.8.0\",\n \"@flue/runtime\": \"0.8.0\",\n \"flue-cli-v0-8-latest\": \"npm:@flue/cli@0.8.1\",\n \"flue-cli-v1\": \"npm:@flue/cli@1.0.0-beta.3\",\n \"flue-cli-v1-latest\": \"npm:@flue/cli@1.0.0-beta.9\",\n \"flue-runtime-v0-8-latest\": \"npm:@flue/runtime@0.8.1\",\n \"flue-runtime-v1\": \"npm:@flue/runtime@1.0.0-beta.3\",\n \"flue-runtime-v1-latest\": \"npm:@flue/runtime@1.0.0-beta.9\",\n \"flue-runtime-v2\": \"npm:@flue/runtime@2.0.0\",\n \"flue-runtime-v2-latest\": \"npm:@flue/runtime@2.0.1\",\n \"pi-ai-v2\": \"npm:@earendil-works/pi-ai@0.83.0\",\n \"valibot\": \"1.4.2\"\n },\n \"pnpm\": {\n \"overrides\": {\n \"@flue/runtime@0.8.0>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.0>@earendil-works/pi-ai\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-agent-core\": \"0.75.4\",\n \"@flue/runtime@0.8.1>@earendil-works/pi-ai\": \"0.75.4\",\n \"@hono/node-server@>=2.0.0 <2.0.10\": \"^2.0.10\",\n \"fast-uri@<3.1.5\": \"^3.1.5\",\n \"hono@>=4.0.0 <4.12.27\": \"^4.12.27\",\n \"brace-expansion@>=3.0.0 <5.0.9\": \"^5.0.9\",\n \"postcss@<8.5.18\": \"^8.5.18\",\n \"valibot@<1.4.2\": \"^1.4.2\",\n \"ip-address@<10.2.2\": \"^10.2.2\",\n \"undici@<7.29.0\": \"^7.29.0\"\n }\n }\n}", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "isError": false, -│ │ │ │ "role": "toolResult", -│ │ │ │ "toolCallId": "", -│ │ │ │ "toolName": "bash" -│ │ │ │ } -│ │ │ │ ] -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "arguments": { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "timeout": 100000 -│ │ │ │ }, -│ │ │ │ "id": "", -│ │ │ │ "name": "bash", -│ │ │ │ "type": "toolCall" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "role": "assistant" -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.api": "openai-responses", -│ │ │ │ "flue.input_message_offset": 15, -│ │ │ │ "flue.input_mode": "delta", -│ │ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ │ "flue.provider": "openai", -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.stop_reason": "toolUse", -│ │ │ │ "flue.turn_purpose": "agent", -│ │ │ │ "model": "gpt-5.4-nano", -│ │ │ │ "provider": "openai" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "completion_tokens": 59, -│ │ │ │ "duration_ms": 0, -│ │ │ │ "estimated_cost": 0.0004814700000000001, -│ │ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ │ "prompt_cached_tokens": 1536, -│ │ │ │ "prompt_tokens": 1885, -│ │ │ │ "tokens": 3480 -│ │ │ │ } -│ │ │ ├── tool:bash [tool] -│ │ │ │ input: { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "timeout": 100000 -│ │ │ │ } -│ │ │ │ output: { -│ │ │ │ "content": [ -│ │ │ │ { -│ │ │ │ "text": "SKILL_DONE", -│ │ │ │ "type": "text" -│ │ │ │ } -│ │ │ │ ], -│ │ │ │ "details": { -│ │ │ │ "command": "printf SKILL_DONE", -│ │ │ │ "exitCode": 0 -│ │ │ │ } -│ │ │ │ } -│ │ │ │ metadata: { -│ │ │ │ "flue.session": "task:skill:", -│ │ │ │ "flue.tool_name": "bash", -│ │ │ │ "provider": "flue" -│ │ │ │ } -│ │ │ │ metrics: { -│ │ │ │ "duration_ms": 0 -│ │ │ │ } -│ │ │ └── flue.turn [llm] -│ │ │ input: [ -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "arguments": { -│ │ │ "command": "printf SKILL_DONE", -│ │ │ "timeout": 100000 -│ │ │ }, -│ │ │ "id": "", -│ │ │ "name": "bash", -│ │ │ "type": "toolCall" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ }, -│ │ │ { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "isError": false, -│ │ │ "role": "toolResult", -│ │ │ "toolCallId": "", -│ │ │ "toolName": "bash" -│ │ │ } -│ │ │ ] -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "textSignature": "{\"v\":1,\"id\":\"msg_054a16a2f2ea3217016a705b70a41481a38ece40ecddf69512\",\"phase\":\"final_answer\"}", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "role": "assistant" -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.api": "openai-responses", -│ │ │ "flue.input_message_offset": 17, -│ │ │ "flue.input_mode": "delta", -│ │ │ "flue.model": "gpt-5.4-nano", -│ │ │ "flue.provider": "openai", -│ │ │ "flue.session": "task:skill:", -│ │ │ "flue.stop_reason": "stop", -│ │ │ "flue.turn_purpose": "agent", -│ │ │ "model": "gpt-5.4-nano", -│ │ │ "provider": "openai" -│ │ │ } -│ │ │ metrics: { -│ │ │ "completion_tokens": 7, -│ │ │ "duration_ms": 0, -│ │ │ "estimated_cost": 0.00031035000000000004, -│ │ │ "prompt_cache_creation_tokens": 0, -│ │ │ "prompt_cached_tokens": 2560, -│ │ │ "prompt_tokens": 1252, -│ │ │ "tokens": 3819 -│ │ │ } -│ │ ├── tool:task [tool] -│ │ │ input: { -│ │ │ "cwd": ".", -│ │ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ │ } -│ │ │ output: { -│ │ │ "content": [ -│ │ │ { -│ │ │ "text": "SKILL_DONE", -│ │ │ "type": "text" -│ │ │ } -│ │ │ ], -│ │ │ "details": { -│ │ │ "cwd": ".", -│ │ │ "messageId": "", -│ │ │ "session": "task:skill:", -│ │ │ "taskId": "" -│ │ │ } -│ │ │ } -│ │ │ metadata: { -│ │ │ "flue.session": "skill", -│ │ │ "flue.tool_name": "task", -│ │ │ "provider": "flue" -│ │ │ } -│ │ │ metrics: { -│ │ │ "duration_ms": 0 -│ │ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\nArguments:\n{\n \"marker\": \"SKILL_DONE\"\n}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "arguments": { -│ │ "cwd": ".", -│ │ "description": "Run the deterministic Flue instrumentation e2e test skill e2e-flue-skill.", -│ │ "prompt": "Run the skill named \"e2e-flue-skill\" with arguments {\"marker\":\"SKILL_DONE\"}. Output only the marker and no extra text when finished." -│ │ }, -│ │ "id": "", -│ │ "name": "task", -│ │ "type": "toolCall" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ }, -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "isError": false, -│ │ "role": "toolResult", -│ │ "toolCallId": "", -│ │ "toolName": "task" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "SKILL_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_0f099b3fce0721fd016a705b71558881a38c53a767e09d9e31\",\"phase\":\"final_answer\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.model": "gpt-5.4-nano", -│ │ "flue.provider": "openai", -│ │ "flue.session": "skill", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-5.4-nano", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 7, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.00020255000000000002, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 969, -│ │ "tokens": 976 -│ │ } -│ ├── flue.task [task] -│ │ input: "Reply with exactly TASK_DONE and no other text." -│ │ output: "TASK_DONE" -│ │ metadata: { -│ │ "flue.session": "task", -│ │ "provider": "flue" -│ │ } -│ │ metrics: { -│ │ "duration_ms": 0 -│ │ } -│ │ └── flue.turn [llm] -│ │ input: [ -│ │ { -│ │ "content": [ -│ │ { -│ │ "text": "Reply with exactly TASK_DONE and no other text.", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "user" -│ │ } -│ │ ] -│ │ output: { -│ │ "content": [ -│ │ { -│ │ "text": "TASK_DONE", -│ │ "textSignature": "{\"v\":1,\"id\":\"msg_0f3adfdb1510c165016a705b72843c81a0b30d242da683a882\"}", -│ │ "type": "text" -│ │ } -│ │ ], -│ │ "role": "assistant" -│ │ } -│ │ metadata: { -│ │ "flue.api": "openai-responses", -│ │ "flue.input_mode": "full", -│ │ "flue.model": "gpt-4o-mini", -│ │ "flue.provider": "openai", -│ │ "flue.session": "task:task:", -│ │ "flue.stop_reason": "stop", -│ │ "flue.turn_purpose": "agent", -│ │ "model": "gpt-4o-mini", -│ │ "provider": "openai" -│ │ } -│ │ metrics: { -│ │ "completion_tokens": 4, -│ │ "duration_ms": 0, -│ │ "estimated_cost": 0.0001263, -│ │ "prompt_cache_creation_tokens": 0, -│ │ "prompt_cached_tokens": 0, -│ │ "prompt_tokens": 826, -│ │ "tokens": 830 -│ │ } -│ └── flue.compact [task] -│ input: { -│ "estimatedTokens": 1229, -│ "reason": "manual" -│ } -│ output: { -│ "completed": true -│ } -│ metadata: { -│ "flue.operation": "compact", -│ "flue.session": "main", -│ "flue.workflow_name": "instrumentation", -│ "provider": "flue", -│ "scenario": "flue-instrumentation" -│ } -│ metrics: { -│ "duration_ms": 0 -│ } -│ └── compaction:manual [task] -│ input: { -│ "estimatedTokens": 1229, -│ "reason": "manual" -│ } -│ output: { -│ "messagesAfter": 2, -│ "messagesBefore": 8 -│ } -│ metadata: { -│ "flue.compaction_reason": "manual", -│ "flue.session": "main", -│ "provider": "flue" -│ } -│ metrics: { -│ "duration_ms": 0, -│ "messages_after": 2, -│ "messages_before": 8 -│ } -│ └── flue.turn [llm] -│ input: [ -│ { -│ "content": [ -│ { -│ "text": "\n[User]: Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.\n\n[Assistant tool calls]: lookup(query=\"flue instrumentation\")\n\n[Tool result]: {\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}\n\n[Assistant tool calls]: web_search(lookupId=\"flue-session-2026\", query=\"Braintrust Flue reasoning stream instrumentation\")\n\n[Tool result]: {\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}\n\n[Assistant tool calls]: summarize_source(url=\"https://example.test/flue/reasoning-streams\")\n\n[Tool result]: {\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}\n\n\nThis is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.\n\nSummarize the prefix to provide context for the retained suffix:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key decisions and work done in the prefix]\n\n## Context for Suffix\n- [Information needed to understand the retained recent work]\n\nBe concise. Focus on what's needed to understand the kept suffix.", -│ "type": "text" -│ } -│ ], -│ "role": "user" -│ } -│ ] -│ output: { -│ "content": [ -│ { -│ "text": "## Original Request\nThe user requested a step-by-step execution of an instrumented research flow involving tool calls to gather information on \"flue instrumentation\" and \"", -│ "textSignature": "{\"v\":1,\"id\":\"msg_04d28ba03840bb65016a705b7358a081a38e69464057840036\"}", -│ "type": "text" -│ } -│ ], -│ "role": "assistant" -│ } -│ metadata: { -│ "flue.api": "openai-responses", -│ "flue.model": "gpt-4o-mini", -│ "flue.provider": "openai", -│ "flue.session": "main", -│ "flue.stop_reason": "stop", -│ "flue.turn_purpose": "compaction_prefix", -│ "model": "gpt-4o-mini", -│ "provider": "openai" -│ } -│ metrics: { -│ "completion_tokens": 0, -│ "duration_ms": 0, -│ "estimated_cost": 0, -│ "prompt_cache_creation_tokens": 0, -│ "prompt_cached_tokens": 0, -│ "prompt_tokens": 0, -│ "tokens": 0 -│ } -└── flue.prompt [task] - input: [ - { - "content": [ - { - "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - "type": "text" - } - ], - "role": "user" - } - ] - output: "PROMPT_DONE" - metadata: { - "flue.operation": "prompt", - "flue.session": "main", - "flue.workflow_name": "instrumentation", - "provider": "flue", - "scenario": "flue-instrumentation" - } - metrics: { - "duration_ms": 0 - } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "text": "Complete this instrumented research flow. Call exactly one tool per turn and wait for each tool result before choosing the next tool. Step 1: call lookup with query \"flue instrumentation\". Step 2: use the lookup result id as lookupId and call web_search with query \"Braintrust Flue reasoning stream instrumentation\". Step 3: use the first web_search result url and call summarize_source. After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_mode": "full", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 19, - │ "duration_ms": 0, - │ "estimated_cost": 0.00022475000000000001, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1005, - │ "tokens": 1024 - │ } - ├── tool:lookup [tool] - │ input: { - │ "query": "flue instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "lookup" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "lookup", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "arguments": { - │ "query": "flue instrumentation" - │ }, - │ "id": "", - │ "name": "lookup", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"id\":\"flue-session-2026\",\"query\":\"flue instrumentation\",\"topic\":\"session instrumentation\"}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "lookup" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_message_offset": 1, - │ "flue.input_mode": "delta", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 34, - │ "duration_ms": 0, - │ "estimated_cost": 0.00025350000000000004, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1055, - │ "tokens": 1089 - │ } - ├── tool:web_search [tool] - │ input: { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "web_search" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "web_search", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - ├── flue.turn [llm] - │ input: [ - │ { - │ "content": [ - │ { - │ "arguments": { - │ "lookupId": "flue-session-2026", - │ "query": "Braintrust Flue reasoning stream instrumentation" - │ }, - │ "id": "", - │ "name": "web_search", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "text": "{\"lookupId\":\"flue-session-2026\",\"query\":\"Braintrust Flue reasoning stream instrumentation\",\"results\":[{\"title\":\"Flue reasoning stream instrumentation\",\"url\":\"https://example.test/flue/reasoning-streams\"}]}", - │ "type": "text" - │ } - │ ], - │ "isError": false, - │ "role": "toolResult", - │ "toolCallId": "", - │ "toolName": "web_search" - │ } - │ ] - │ output: { - │ "content": [ - │ { - │ "arguments": { - │ "url": "https://example.test/flue/reasoning-streams" - │ }, - │ "id": "", - │ "name": "summarize_source", - │ "type": "toolCall" - │ } - │ ], - │ "role": "assistant" - │ } - │ metadata: { - │ "flue.api": "openai-responses", - │ "flue.input_message_offset": 3, - │ "flue.input_mode": "delta", - │ "flue.model": "gpt-5.4-nano", - │ "flue.provider": "openai", - │ "flue.session": "main", - │ "flue.stop_reason": "toolUse", - │ "flue.turn_purpose": "agent", - │ "model": "gpt-5.4-nano", - │ "provider": "openai" - │ } - │ metrics: { - │ "completion_tokens": 30, - │ "duration_ms": 0, - │ "estimated_cost": 0.00026690000000000004, - │ "prompt_cache_creation_tokens": 0, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 1147, - │ "tokens": 1177 - │ } - ├── tool:summarize_source [tool] - │ input: { - │ "url": "https://example.test/flue/reasoning-streams" - │ } - │ output: { - │ "content": [ - │ { - │ "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - │ "type": "text" - │ } - │ ], - │ "details": { - │ "customTool": "summarize_source" - │ } - │ } - │ metadata: { - │ "flue.session": "main", - │ "flue.tool_name": "summarize_source", - │ "provider": "flue" - │ } - │ metrics: { - │ "duration_ms": 0 - │ } - └── flue.turn [llm] - input: [ - { - "content": [ - { - "arguments": { - "url": "https://example.test/flue/reasoning-streams" - }, - "id": "", - "name": "summarize_source", - "type": "toolCall" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "text": "{\"summary\":\"Flue emits reasoning, tool execution, and LLM turn events separately.\",\"url\":\"https://example.test/flue/reasoning-streams\"}", - "type": "text" - } - ], - "isError": false, - "role": "toolResult", - "toolCallId": "", - "toolName": "summarize_source" - } - ] - output: { - "content": [ - { - "text": "PROMPT_DONE", - "textSignature": "{\"v\":1,\"id\":\"msg_045458052dc10379016a705b62dbd8819189b8287061debf12\",\"phase\":\"final_answer\"}", - "type": "text" - } - ], - "role": "assistant" - } - metadata: { - "flue.api": "openai-responses", - "flue.input_message_offset": 5, - "flue.input_mode": "delta", - "flue.model": "gpt-5.4-nano", - "flue.provider": "openai", - "flue.session": "main", - "flue.stop_reason": "stop", - "flue.turn_purpose": "agent", - "model": "gpt-5.4-nano", - "provider": "openai" - } - metrics: { - "completion_tokens": 7, - "duration_ms": 0, - "estimated_cost": 0.00025315000000000005, - "prompt_cache_creation_tokens": 0, - "prompt_cached_tokens": 0, - "prompt_tokens": 1222, - "tokens": 1229 - } diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-0-0-beta-3-explicit.span-tree.json b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-0-0-beta-3-explicit.span-tree.json index 464358cef..aa0fef2c7 100644 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-0-0-beta-3-explicit.span-tree.json +++ b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-0-0-beta-3-explicit.span-tree.json @@ -75,7 +75,7 @@ "input": { "name": "e2e-flue-skill" }, - "output": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-v1/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", + "output": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-instrumentation/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", "metadata": { "flue.session": "skill", "flue.tool_name": "activate_skill", @@ -119,7 +119,7 @@ { "content": [ { - "text": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-v1/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", + "text": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-instrumentation/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", "type": "text" } ], diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-0-0-beta-3-explicit.span-tree.txt b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-0-0-beta-3-explicit.span-tree.txt index 104f75f29..60b2ac825 100644 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-0-0-beta-3-explicit.span-tree.txt +++ b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-0-0-beta-3-explicit.span-tree.txt @@ -99,7 +99,7 @@ span_tree: │ │ │ input: { │ │ │ "name": "e2e-flue-skill" │ │ │ } -│ │ │ output: "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-v1/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n" +│ │ │ output: "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-instrumentation/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n" │ │ │ metadata: { │ │ │ "flue.session": "skill", │ │ │ "flue.tool_name": "activate_skill", @@ -139,7 +139,7 @@ span_tree: │ │ { │ │ "content": [ │ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-v1/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", +│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-instrumentation/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", │ │ "type": "text" │ │ } │ │ ], diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-latest-explicit.span-tree.json b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-latest-explicit.span-tree.json index ac0646db1..b4336fbab 100644 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-latest-explicit.span-tree.json +++ b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-latest-explicit.span-tree.json @@ -71,7 +71,7 @@ "input": { "name": "e2e-flue-skill" }, - "output": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-v1/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", + "output": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-instrumentation/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", "metadata": { "flue.session": "skill", "flue.tool_name": "activate_skill", @@ -111,7 +111,7 @@ { "content": [ { - "text": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-v1/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", + "text": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-instrumentation/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", "type": "text" } ], diff --git a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-latest-explicit.span-tree.txt b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-latest-explicit.span-tree.txt index 2a17573af..dd492f42e 100644 --- a/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-latest-explicit.span-tree.txt +++ b/e2e/scenarios/flue-instrumentation/__snapshots__/flue-v1-latest-explicit.span-tree.txt @@ -95,7 +95,7 @@ span_tree: │ │ │ input: { │ │ │ "name": "e2e-flue-skill" │ │ │ } -│ │ │ output: "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-v1/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n" +│ │ │ output: "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-instrumentation/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n" │ │ │ metadata: { │ │ │ "flue.session": "skill", │ │ │ "flue.tool_name": "activate_skill", @@ -131,7 +131,7 @@ span_tree: │ │ { │ │ "content": [ │ │ { -│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-v1/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", +│ │ "text": "Run the skill named \"e2e-flue-skill\".\n\n\nReturn the marker from the provided args exactly once. Output no other text.\n\n\nSupporting skill resources are available relative to this workspace skill directory but are not loaded into context unless needed:\n\n- Base directory: /e2e/scenarios/flue-instrumentation/.agents/skills/e2e-flue-skill\n- Resolve relative resource paths from this directory and read only the files you need.\n", │ │ "type": "text" │ │ } │ │ ], diff --git a/e2e/scenarios/flue-instrumentation/assertions.ts b/e2e/scenarios/flue-instrumentation/assertions.ts index 53a597fbd..89b564836 100644 --- a/e2e/scenarios/flue-instrumentation/assertions.ts +++ b/e2e/scenarios/flue-instrumentation/assertions.ts @@ -20,14 +20,6 @@ import { import { SCENARIO_NAME } from "./constants.mjs"; type RunFlueScenario = (harness: { - runNodeScenarioDir: (options: { - entry: string; - env?: Record; - nodeArgs?: string[]; - runContext?: ScenarioRunContext; - scenarioDir: string; - timeoutMs: number; - }) => Promise; runScenarioDir: (options: { entry: string; env?: Record; @@ -155,40 +147,24 @@ function firstSpanIndex( return index === -1 ? Number.MAX_SAFE_INTEGER : index; } -function findFlueDescendants( - events: CapturedLogEvent[], - flueSpan: CapturedLogEvent | undefined, - predicate: (event: CapturedLogEvent) => boolean, -): CapturedLogEvent[] { - return findMatchingDescendants(events, flueSpan, predicate); -} - -function isFlueChildSpan( - event: CapturedLogEvent, - includeAmbientProbeSpans = true, -): boolean { +function isFlueChildSpan(event: CapturedLogEvent): boolean { return ( event.span.name === "flue.turn" || event.span.name?.startsWith("tool:") === true || event.span.name?.startsWith("task:") === true || event.span.name?.startsWith("compaction:") === true || - (includeAmbientProbeSpans && event.span.name === "flue.toolCurrentProbe") || + event.span.name === "flue.toolCurrentProbe" || event.span.name === "flue.task" ); } -function buildSpanTree( - events: CapturedLogEvent[], - includeAmbientProbeSpans: boolean, -): SpanTreeEntry[] { +function buildSpanTree(events: CapturedLogEvent[]): SpanTreeEntry[] { const workflow = findLatestSpanByPrefix(events, "workflow:"); - const workflowCurrentProbe = includeAmbientProbeSpans - ? findLatestChildSpan( - events, - "flue.workflowCurrentProbe", - workflow?.span.id, - ) - : undefined; + const workflowCurrentProbe = findLatestChildSpan( + events, + "flue.workflowCurrentProbe", + workflow?.span.id, + ); const promptSpan = findFlueOperation(events, "flue.prompt"); const skillSpan = findFlueOperation(events, "flue.skill"); const taskSpan = findFlueOperation(events, "flue.task"); @@ -198,28 +174,19 @@ function buildSpanTree( workflow, workflowCurrentProbe, promptSpan, - ...findFlueDescendants(events, promptSpan, (event) => - isFlueChildSpan(event, includeAmbientProbeSpans), - ), + ...findMatchingDescendants(events, promptSpan, isFlueChildSpan), skillSpan, - ...findFlueDescendants(events, skillSpan, (event) => - isFlueChildSpan(event, includeAmbientProbeSpans), - ), + ...findMatchingDescendants(events, skillSpan, isFlueChildSpan), taskSpan, - ...findFlueDescendants(events, taskSpan, (event) => - isFlueChildSpan(event, includeAmbientProbeSpans), - ), + ...findMatchingDescendants(events, taskSpan, isFlueChildSpan), compactSpan, - ...findFlueDescendants(events, compactSpan, (event) => - isFlueChildSpan(event, includeAmbientProbeSpans), - ), + ...findMatchingDescendants(events, compactSpan, isFlueChildSpan), ].flatMap((event) => event ? [{ event, fields: snapshotFields(event) }] : [], ); } export function defineFlueInstrumentationAssertions(options: { - expectAmbientContext?: boolean; name: string; runScenario: RunFlueScenario; snapshotName: string; @@ -232,7 +199,6 @@ export function defineFlueInstrumentationAssertions(options: { ); const timeoutMs = effectiveScenarioTimeoutMs(options.timeoutMs); const testConfig = { timeout: timeoutMs }; - const expectAmbientContext = options.expectAmbientContext ?? true; describe.sequential(options.name, () => { let events: CapturedLogEvent[] = []; @@ -257,46 +223,42 @@ export function defineFlueInstrumentationAssertions(options: { }); }); - if (expectAmbientContext) { - test( - "makes the Flue workflow span current for app spans", - testConfig, - () => { - const workflow = findLatestSpanByPrefix(events, "workflow:"); - const appSpan = findLatestChildSpan( - events, - "flue.workflowCurrentProbe", - workflow?.span.id, - ); - - expect(appSpan).toBeDefined(); - expect(appSpan?.span.parentIds).toEqual([workflow?.span.id]); - expect(appSpan?.output).toBe("active"); - }, - ); - - test("makes Flue tool spans current for app spans", testConfig, () => { + test( + "makes the Flue workflow span current for app spans", + testConfig, + () => { const workflow = findLatestSpanByPrefix(events, "workflow:"); - const promptSpan = findFlueOperation(events, "flue.prompt"); - const lookupToolSpan = findFlueDescendants( - events, - promptSpan, - (event) => event.span.name === "tool:lookup", - )[0]; const appSpan = findLatestChildSpan( events, - "flue.toolCurrentProbe", - lookupToolSpan?.span.id, + "flue.workflowCurrentProbe", + workflow?.span.id, ); expect(appSpan).toBeDefined(); - expect(appSpan?.span.parentIds).toEqual([lookupToolSpan?.span.id]); - expect(appSpan?.output).toBe("lookup-active"); - expect(workflow).toBeDefined(); - }); - } + expect(appSpan?.span.parentIds).toEqual([workflow?.span.id]); + expect(appSpan?.output).toBe("active"); + }, + ); + + test("makes Flue tool spans current for app spans", testConfig, () => { + const promptSpan = findFlueOperation(events, "flue.prompt"); + const lookupToolSpan = findMatchingDescendants( + events, + promptSpan, + (event) => event.span.name === "tool:lookup", + )[0]; + const appSpan = findLatestChildSpan( + events, + "flue.toolCurrentProbe", + lookupToolSpan?.span.id, + ); - test("captures observe-based Flue operation spans", testConfig, () => { + expect(appSpan).toBeDefined(); + expect(appSpan?.span.parentIds).toEqual([lookupToolSpan?.span.id]); + expect(appSpan?.output).toBe("lookup-active"); + }); + + test("captures Flue operation spans", testConfig, () => { for (const flueSpanName of [ "flue.prompt", "flue.skill", @@ -334,10 +296,10 @@ export function defineFlueInstrumentationAssertions(options: { testConfig, () => { const promptSpan = findFlueOperation(events, "flue.prompt"); - const promptChildren = findFlueDescendants( + const promptChildren = findMatchingDescendants( events, promptSpan, - (event) => isFlueChildSpan(event, expectAmbientContext), + isFlueChildSpan, ); const promptTurns = promptChildren.filter( (event) => event.span.name === "flue.turn", @@ -349,7 +311,7 @@ export function defineFlueInstrumentationAssertions(options: { const compactSpan = findFlueOperation(events, "flue.compact"); const allLlmSpans = [promptSpan, skillSpan, compactSpan].flatMap( (span) => - findFlueDescendants( + findMatchingDescendants( events, span, (event) => event.span.name === "flue.turn", @@ -359,12 +321,12 @@ export function defineFlueInstrumentationAssertions(options: { (event) => event.span.name === "tool:lookup", ); const taskSpan = findFlueOperation(events, "flue.task"); - const nestedTaskSpans = findFlueDescendants( + const nestedTaskSpans = findMatchingDescendants( events, taskSpan, (event) => event.span.name === "flue.task", ); - const compaction = findFlueDescendants( + const compaction = findMatchingDescendants( events, compactSpan, (event) => event.span.name?.startsWith("compaction:") === true, @@ -414,15 +376,11 @@ export function defineFlueInstrumentationAssertions(options: { }); test("matches the span tree snapshot", testConfig, async () => { - await matchSpanTreeSnapshot( - buildSpanTree(events, expectAmbientContext), - snapshotPath, - { - normalize: { - additionalProviderIdKeys: ["messageId"], - }, + await matchSpanTreeSnapshot(buildSpanTree(events), snapshotPath, { + normalize: { + additionalProviderIdKeys: ["messageId"], }, - ); + }); }); }); } diff --git a/e2e/scenarios/flue-instrumentation/package.json b/e2e/scenarios/flue-instrumentation/package.json index 456dcc68f..5a98aeb76 100644 --- a/e2e/scenarios/flue-instrumentation/package.json +++ b/e2e/scenarios/flue-instrumentation/package.json @@ -5,19 +5,11 @@ "braintrustScenario": { "bump": { "dependencies": { - "flue-cli-v0-8-latest": { - "package": "@flue/cli", - "range": ">=0.8.0 <0.9.0" - }, "flue-cli-v1-latest": { "package": "@flue/cli", "range": ">=1.0.0-beta.3 <2.0.0-0", "allowPrerelease": true }, - "flue-runtime-v0-8-latest": { - "package": "@flue/runtime", - "range": ">=0.8.0 <0.9.0" - }, "flue-runtime-v1-latest": { "package": "@flue/runtime", "range": ">=1.0.0-beta.3 <2.0.0-0", @@ -31,13 +23,9 @@ } }, "dependencies": { - "@flue/cli": "0.8.0", - "@flue/runtime": "0.8.0", - "flue-cli-v0-8-latest": "npm:@flue/cli@0.8.1", - "flue-cli-v1": "npm:@flue/cli@1.0.0-beta.3", + "@flue/cli": "1.0.0-beta.3", + "@flue/runtime": "1.0.0-beta.3", "flue-cli-v1-latest": "npm:@flue/cli@1.0.0-beta.9", - "flue-runtime-v0-8-latest": "npm:@flue/runtime@0.8.1", - "flue-runtime-v1": "npm:@flue/runtime@1.0.0-beta.3", "flue-runtime-v1-latest": "npm:@flue/runtime@1.0.0-beta.9", "flue-runtime-v2": "npm:@flue/runtime@2.0.0", "flue-runtime-v2-latest": "npm:@flue/runtime@2.0.1", @@ -46,10 +34,6 @@ }, "pnpm": { "overrides": { - "@flue/runtime@0.8.0>@earendil-works/pi-agent-core": "0.75.4", - "@flue/runtime@0.8.0>@earendil-works/pi-ai": "0.75.4", - "@flue/runtime@0.8.1>@earendil-works/pi-agent-core": "0.75.4", - "@flue/runtime@0.8.1>@earendil-works/pi-ai": "0.75.4", "@hono/node-server@>=2.0.0 <2.0.10": "^2.0.10", "fast-uri@<3.1.5": "^3.1.5", "hono@>=4.0.0 <4.12.27": "^4.12.27", diff --git a/e2e/scenarios/flue-instrumentation/pnpm-lock.yaml b/e2e/scenarios/flue-instrumentation/pnpm-lock.yaml index d1b7e728c..d1eec91bd 100644 --- a/e2e/scenarios/flue-instrumentation/pnpm-lock.yaml +++ b/e2e/scenarios/flue-instrumentation/pnpm-lock.yaml @@ -5,10 +5,6 @@ settings: excludeLinksFromLockfile: false overrides: - '@flue/runtime@0.8.0>@earendil-works/pi-agent-core': 0.75.4 - '@flue/runtime@0.8.0>@earendil-works/pi-ai': 0.75.4 - '@flue/runtime@0.8.1>@earendil-works/pi-agent-core': 0.75.4 - '@flue/runtime@0.8.1>@earendil-works/pi-ai': 0.75.4 '@hono/node-server@>=2.0.0 <2.0.10': ^2.0.10 fast-uri@<3.1.5: ^3.1.5 hono@>=4.0.0 <4.12.27: ^4.12.27 @@ -23,38 +19,26 @@ importers: .: dependencies: '@flue/cli': - specifier: 0.8.0 - version: 0.8.0(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(typebox@1.3.7)(workerd@1.20260730.1)(wrangler@4.95.0)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) + specifier: 1.0.0-beta.3 + version: 1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(hono@4.12.31)(typebox@1.3.7)(wrangler@4.95.0)(ws@8.21.0)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) '@flue/runtime': - specifier: 0.8.0 - version: 0.8.0(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) - flue-cli-v0-8-latest: - specifier: npm:@flue/cli@0.8.1 - version: '@flue/cli@0.8.1(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(typebox@1.3.7)(workerd@1.20260730.1)(wrangler@4.95.0)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)' - flue-cli-v1: - specifier: npm:@flue/cli@1.0.0-beta.3 - version: '@flue/cli@1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(hono@4.12.31)(typebox@1.3.7)(wrangler@4.95.0)(ws@8.20.1)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)' + specifier: 1.0.0-beta.3 + version: 1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) flue-cli-v1-latest: specifier: npm:@flue/cli@1.0.0-beta.9 - version: '@flue/cli@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(hono@4.12.31)(typebox@1.3.7)(wrangler@4.95.0)(ws@8.20.1)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)' - flue-runtime-v0-8-latest: - specifier: npm:@flue/runtime@0.8.1 - version: '@flue/runtime@0.8.1(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)' - flue-runtime-v1: - specifier: npm:@flue/runtime@1.0.0-beta.3 - version: '@flue/runtime@1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.20.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)' + version: '@flue/cli@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(hono@4.12.31)(typebox@1.3.7)(wrangler@4.95.0)(ws@8.21.0)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)' flue-runtime-v1-latest: specifier: npm:@flue/runtime@1.0.0-beta.9 - version: '@flue/runtime@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.20.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)' + version: '@flue/runtime@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)' flue-runtime-v2: specifier: npm:@flue/runtime@2.0.0 - version: '@flue/runtime@2.0.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)' + version: '@flue/runtime@2.0.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)' flue-runtime-v2-latest: specifier: npm:@flue/runtime@2.0.1 - version: '@flue/runtime@2.0.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)' + version: '@flue/runtime@2.0.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)' pi-ai-v2: specifier: npm:@earendil-works/pi-ai@0.83.0 - version: '@earendil-works/pi-ai@0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)' + version: '@earendil-works/pi-ai@0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)' valibot: specifier: 1.4.2 version: 1.4.2 @@ -191,12 +175,6 @@ packages: workerd: optional: true - '@cloudflare/vite-plugin@1.39.0': - resolution: {integrity: sha512-AHC+KSR+3dtGu7Ab7I0Ode4Whx12TxMEmiZt7w+Fc3/2wYNByIzbb6cndWZ78tnveFdO1xhNLv1YaNngxGtOPg==} - peerDependencies: - vite: ^6.1.0 || ^7.0.0 || ^8.0.0 - wrangler: ^4.95.0 - '@cloudflare/vite-plugin@1.50.0': resolution: {integrity: sha512-zIhZim7Kr7OC22rlOkU81BLj0oRlUOqPRX+E6RU3hyRYg4xU1MbA3yiMIwFONr+jc/uV7Fxs20NlXrB89Fqjqg==} hasBin: true @@ -273,10 +251,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@earendil-works/pi-agent-core@0.75.4': - resolution: {integrity: sha512-cGYbysb4EqUf0B28OeqFq2ppm1XF3bYBOP71q9dv38yf/UJfzMjiXBeNelrcio+QWIoVrW+xzYm7sMzYIUc9Og==} - engines: {node: '>=22.19.0'} - '@earendil-works/pi-agent-core@0.79.10': resolution: {integrity: sha512-XKxgdjhcPuyjrthCOFSgfzT3xZ1uBrJ1IMVDxci1to6hIN6BIg9J5iY8q0pGXK1DLgATLP23da+1UyZLwA360Q==} engines: {node: '>=22.19.0'} @@ -289,11 +263,6 @@ packages: resolution: {integrity: sha512-RorGp9OH5l3ElpuC5a5ZQ2eWcchZGXflXRzVGkV99y3y6tT+LLNyxoYIdVKvTKWEObwhExeQbTH0fI2tE4iX4g==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.75.4': - resolution: {integrity: sha512-m/w8Hh3vQ0rAycwJiJWdzkypkn4295f4eq/966lDRy8aX5sk6bgYXH8TQmL16TO7Uwc7MbJG0QoyFHgX8RqXUQ==} - engines: {node: '>=22.19.0'} - hasBin: true - '@earendil-works/pi-ai@0.79.10': resolution: {integrity: sha512-9jR23tOl0BIUdQMn70Gr72xYBpM7Xgl9Lyv7gAnU1USfkNRuYG/f/edLl+n/Dp/RafDW3JI4DF7y/GhgkORuew==} engines: {node: '>=22.19.0'} @@ -477,26 +446,6 @@ packages: cpu: [x64] os: [win32] - '@flue/cli@0.8.0': - resolution: {integrity: sha512-iWTVoSCZ0/D5T5O4E+dha9IT7yx1/qqjsEGLtedd/wnlp2nK45qTrjutBS2pgenGNzdQ6wlPzMPkP8MA9z49yw==} - engines: {node: '>=22.18.0'} - hasBin: true - peerDependencies: - wrangler: ^4.94.0 - peerDependenciesMeta: - wrangler: - optional: true - - '@flue/cli@0.8.1': - resolution: {integrity: sha512-8aeaSf7RsXj4A4P+eQgTj8l2+17NScrVCK3UcbMuQ2pwAu84v5t6SQR2XvYuqActlidTIFR6+xxWS7dR9T7yZw==} - engines: {node: '>=22.18.0'} - hasBin: true - peerDependencies: - wrangler: ^4.94.0 - peerDependenciesMeta: - wrangler: - optional: true - '@flue/cli@1.0.0-beta.3': resolution: {integrity: sha512-i8Lo0koB0gcy3bbCp+kwA1C522PsxvN8ydexDwR3rMkWisitG3FVmqmsLPkWBetspmXBLDbyDq39+AdSoXf4EQ==} engines: {node: '>=22.19.0'} @@ -507,14 +456,6 @@ packages: engines: {node: '>=22.19.0'} hasBin: true - '@flue/runtime@0.8.0': - resolution: {integrity: sha512-GCXJOZtb6s+HuMBzB4I41uQzxX7pELfdorD3gnWnkYoNFjHW8D/ncjXMTsczyl2tkP1IPK0C1Ffw0zwGqkmO7g==} - engines: {node: '>=22.18.0'} - - '@flue/runtime@0.8.1': - resolution: {integrity: sha512-nhIiNLr4NmsK6xgYgFt+mTFTKhHxMn++4gjzygYQIUqQK0GR4hgIjpvosl/361Xx087+8N0wNWh2MEVStoZCIg==} - engines: {node: '>=22.18.0'} - '@flue/runtime@1.0.0-beta.3': resolution: {integrity: sha512-VkWz6lRaS73fu6Y8Wh+CjAHjrLmICUmDwURQCA7dtXWP2O99UyvDQ/5Cbg9DYpsYZpaFuOhWzOCj+Igc4F/LbA==} engines: {node: '>=22.19.0'} @@ -903,9 +844,6 @@ packages: '@microsoft/fetch-event-source@2.0.1': resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} - '@mistralai/mistralai@2.2.1': - resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} - '@mistralai/mistralai@2.2.6': resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} peerDependencies: @@ -2002,11 +1940,6 @@ packages: resolution: {integrity: sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -2154,10 +2087,6 @@ packages: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} - engines: {node: '>=20.18.1'} - unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -2553,19 +2482,6 @@ snapshots: optionalDependencies: workerd: 1.20260730.1 - '@cloudflare/vite-plugin@1.39.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0))(workerd@1.20260730.1)(wrangler@4.95.0)': - dependencies: - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260730.1) - miniflare: 4.20260526.0 - unenv: 2.0.0-rc.24 - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0) - wrangler: 4.95.0 - ws: 8.20.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - workerd - '@cloudflare/vite-plugin@1.50.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0))(wrangler@4.95.0)': dependencies: '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260730.1) @@ -2618,9 +2534,9 @@ snapshots: '@microsoft/fetch-event-source': 2.0.1 fastq: 1.20.1 - '@earendil-works/pi-agent-core@0.75.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.75.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-ai': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -2632,9 +2548,9 @@ snapshots: - ws - zod - '@earendil-works/pi-agent-core@0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -2646,23 +2562,9 @@ snapshots: - ws - zod - '@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - ignore: 7.0.5 - typebox: 1.1.38 - yaml: 2.9.0 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@earendil-works/pi-agent-core@0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': - dependencies: - '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) diff: 8.0.4 ignore: 7.0.5 typebox: 1.3.7 @@ -2675,26 +2577,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.75.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(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)) - '@mistralai/mistralai': 2.2.1 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.20.1)(zod@4.4.3) - partial-json: 0.1.7 - typebox: 1.1.38 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@earendil-works/pi-ai@0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + '@earendil-works/pi-ai@0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -2704,7 +2587,7 @@ snapshots: '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.20.1)(zod@4.4.3) + openai: 6.26.0(ws@8.21.0)(zod@4.4.3) partial-json: 0.1.7 typebox: 1.1.38 transitivePeerDependencies: @@ -2715,7 +2598,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + '@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -2725,7 +2608,7 @@ snapshots: '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.20.1)(zod@4.4.3) + openai: 6.26.0(ws@8.21.0)(zod@4.4.3) partial-json: 0.1.7 typebox: 1.1.38 transitivePeerDependencies: @@ -2736,7 +2619,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + '@earendil-works/pi-ai@0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -2746,7 +2629,7 @@ snapshots: '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.20.1)(zod@4.4.3) + openai: 6.26.0(ws@8.21.0)(zod@4.4.3) partial-json: 0.1.7 typebox: 1.3.7 transitivePeerDependencies: @@ -2856,88 +2739,10 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@flue/cli@0.8.0(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(typebox@1.3.7)(workerd@1.20260730.1)(wrangler@4.95.0)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': - dependencies: - '@cloudflare/vite-plugin': 1.39.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0))(workerd@1.20260730.1)(wrangler@4.95.0) - '@flue/runtime': 0.8.0(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) - '@vercel/detect-agent': 1.2.3 - package-up: 5.0.0 - valibot: 1.4.2 - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0) - optionalDependencies: - wrangler: 4.95.0 - transitivePeerDependencies: - - '@cfworker/json-schema' - - '@standard-schema/spec' - - '@types/json-schema' - - '@types/node' - - '@vitejs/devtools' - - arktype - - bufferutil - - effect - - esbuild - - jiti - - less - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - sury - - terser - - tsx - - typebox - - typescript - - utf-8-validate - - workerd - - yaml - - zod - - zod-openapi - - zod-to-json-schema - - '@flue/cli@0.8.1(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(typebox@1.3.7)(workerd@1.20260730.1)(wrangler@4.95.0)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': - dependencies: - '@cloudflare/vite-plugin': 1.39.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0))(workerd@1.20260730.1)(wrangler@4.95.0) - '@flue/runtime': 0.8.1(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) - '@vercel/detect-agent': 1.2.3 - package-up: 5.0.0 - valibot: 1.4.2 - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0) - optionalDependencies: - wrangler: 4.95.0 - transitivePeerDependencies: - - '@cfworker/json-schema' - - '@standard-schema/spec' - - '@types/json-schema' - - '@types/node' - - '@vitejs/devtools' - - arktype - - bufferutil - - effect - - esbuild - - jiti - - less - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - sury - - terser - - tsx - - typebox - - typescript - - utf-8-validate - - workerd - - yaml - - zod - - zod-openapi - - zod-to-json-schema - - '@flue/cli@1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(hono@4.12.31)(typebox@1.3.7)(wrangler@4.95.0)(ws@8.20.1)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': + '@flue/cli@1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(hono@4.12.31)(typebox@1.3.7)(wrangler@4.95.0)(ws@8.21.0)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': dependencies: '@cloudflare/vite-plugin': 1.50.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0))(wrangler@4.95.0) - '@flue/runtime': 1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.20.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) + '@flue/runtime': 1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) '@flue/sdk': 1.0.0-beta.3 '@hono/node-server': 2.0.11(hono@4.12.31) '@vercel/detect-agent': 1.2.3 @@ -2979,10 +2784,10 @@ snapshots: - zod-openapi - zod-to-json-schema - '@flue/cli@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(hono@4.12.31)(typebox@1.3.7)(wrangler@4.95.0)(ws@8.20.1)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': + '@flue/cli@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@25.9.1)(esbuild@0.27.3)(hono@4.12.31)(typebox@1.3.7)(wrangler@4.95.0)(ws@8.21.0)(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': dependencies: '@cloudflare/vite-plugin': 1.50.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.3)(yaml@2.9.0))(wrangler@4.95.0) - '@flue/runtime': 1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.20.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) + '@flue/runtime': 1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) '@flue/sdk': 1.0.0-beta.9 '@hono/node-server': 2.0.11(hono@4.12.31) '@vercel/detect-agent': 1.2.3 @@ -3024,80 +2829,10 @@ snapshots: - zod-openapi - zod-to-json-schema - '@flue/runtime@0.8.0(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': - dependencies: - '@earendil-works/pi-agent-core': 0.75.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-ai': 0.75.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@hono/node-server': 2.0.11(hono@4.12.31) - '@hono/standard-validator': 0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.31) - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.0(valibot@1.4.2))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) - '@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.0(valibot@1.4.2))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2)(zod@4.4.3) - '@valibot/to-json-schema': 1.7.0(valibot@1.4.2) - hono: 4.12.31 - hono-openapi: 1.3.0(@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.31))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.0(valibot@1.4.2))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.0(valibot@1.4.2))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2)(zod@4.4.3))(@types/json-schema@7.0.15)(hono@4.12.31)(openapi-types@12.1.3) - js-yaml: 4.1.1 - just-bash: 3.0.1 - openapi-types: 12.1.3 - quansync: 0.2.11 - ulidx: 2.4.1 - valibot: 1.4.2 - ws: 8.20.1 - transitivePeerDependencies: - - '@cfworker/json-schema' - - '@standard-schema/spec' - - '@types/json-schema' - - arktype - - bufferutil - - effect - - supports-color - - sury - - typebox - - typescript - - utf-8-validate - - zod - - zod-openapi - - zod-to-json-schema - - '@flue/runtime@0.8.1(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': - dependencies: - '@earendil-works/pi-agent-core': 0.75.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-ai': 0.75.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@hono/node-server': 2.0.11(hono@4.12.31) - '@hono/standard-validator': 0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.31) - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.0(valibot@1.4.2))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) - '@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.0(valibot@1.4.2))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2)(zod@4.4.3) - '@valibot/to-json-schema': 1.7.0(valibot@1.4.2) - hono: 4.12.31 - hono-openapi: 1.3.0(@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.31))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.0(valibot@1.4.2))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.0(valibot@1.4.2))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2)(zod@4.4.3))(@types/json-schema@7.0.15)(hono@4.12.31)(openapi-types@12.1.3) - js-yaml: 4.1.1 - just-bash: 3.0.1 - openapi-types: 12.1.3 - quansync: 0.2.11 - ulidx: 2.4.1 - valibot: 1.4.2 - ws: 8.20.1 - transitivePeerDependencies: - - '@cfworker/json-schema' - - '@standard-schema/spec' - - '@types/json-schema' - - arktype - - bufferutil - - effect - - supports-color - - sury - - typebox - - typescript - - utf-8-validate - - zod - - zod-openapi - - zod-to-json-schema - - '@flue/runtime@1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.20.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': + '@flue/runtime@1.0.0-beta.3(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-ai': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-agent-core': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@hono/node-server': 2.0.11(hono@4.12.31) '@hono/standard-validator': 0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.31) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) @@ -3129,10 +2864,10 @@ snapshots: - zod-openapi - zod-to-json-schema - '@flue/runtime@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.20.1)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': + '@flue/runtime@1.0.0-beta.9(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(typebox@1.3.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@hono/node-server': 2.0.11(hono@4.12.31) '@hono/standard-validator': 0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.31) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) @@ -3164,10 +2899,10 @@ snapshots: - zod-openapi - zod-to-json-schema - '@flue/runtime@2.0.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + '@flue/runtime@2.0.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@hono/node-server': 2.0.11(hono@4.12.31) '@modelcontextprotocol/client': 2.0.0 '@valibot/to-json-schema': 1.7.0(valibot@1.4.2) @@ -3184,10 +2919,10 @@ snapshots: - ws - zod - '@flue/runtime@2.0.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + '@flue/runtime@2.0.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@hono/node-server': 2.0.11(hono@4.12.31) '@modelcontextprotocol/client': 2.0.0 '@valibot/to-json-schema': 1.7.0(valibot@1.4.2) @@ -3407,7 +3142,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.3 optional: true '@img/sharp-wasm32@0.35.2': @@ -3467,15 +3202,6 @@ snapshots: '@microsoft/fetch-event-source@2.0.1': {} - '@mistralai/mistralai@2.2.1': - dependencies: - ws: 8.20.1 - zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/semantic-conventions': 1.43.0 @@ -4303,7 +4029,7 @@ snapshots: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.35.2 - undici: 7.28.0 + undici: 7.29.0 workerd: 1.20260730.1 ws: 8.21.0 youch: 4.1.0-beta.10 @@ -4336,7 +4062,7 @@ snapshots: node-abi@3.92.0: dependencies: - semver: 7.8.0 + semver: 7.8.5 optional: true node-addon-api@8.7.0: @@ -4371,9 +4097,9 @@ snapshots: dependencies: wrappy: 1.0.2 - openai@6.26.0(ws@8.20.1)(zod@4.4.3): + openai@6.26.0(ws@8.21.0)(zod@4.4.3): optionalDependencies: - ws: 8.20.1 + ws: 8.21.0 zod: 4.4.3 openapi-types@12.1.3: {} @@ -4561,8 +4287,6 @@ snapshots: dependencies: commander: 6.2.1 - semver@7.8.0: {} - semver@7.8.5: {} send@1.2.1: @@ -4596,7 +4320,7 @@ snapshots: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.0 + semver: 7.8.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -4788,8 +4512,6 @@ snapshots: undici@7.29.0: {} - undici@7.28.0: {} - unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 diff --git a/e2e/scenarios/flue-instrumentation/scenario.cli.mjs b/e2e/scenarios/flue-instrumentation/scenario.cli.mjs deleted file mode 100644 index bbbb961fd..000000000 --- a/e2e/scenarios/flue-instrumentation/scenario.cli.mjs +++ /dev/null @@ -1,4 +0,0 @@ -const { runCliFlueInstrumentation, runMain } = - await import("./scenario.impl.mjs"); - -runMain(runCliFlueInstrumentation); diff --git a/e2e/scenarios/flue-instrumentation/scenario.impl.mjs b/e2e/scenarios/flue-instrumentation/scenario.impl.mjs index e0c4a5ce2..ba30b1543 100644 --- a/e2e/scenarios/flue-instrumentation/scenario.impl.mjs +++ b/e2e/scenarios/flue-instrumentation/scenario.impl.mjs @@ -1,5 +1,4 @@ import { spawn } from "node:child_process"; -import { access, rm } from "node:fs/promises"; import { createServer } from "node:net"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -15,7 +14,6 @@ const flueCliPath = path.join( "bin", "flue.mjs", ); -const braintrustHookNodeOption = "--import=braintrust/hook.mjs"; function workflowPayload() { return { @@ -27,27 +25,23 @@ function workflowPayload() { }; } -export async function runNodeFlueInstrumentationScenario(options) { - const env = scenarioEnv(options); - const outputDir = path.join(process.cwd(), ".flue-build", options.outputName); - await runFlueCli( - [ - "build", - "--target", - "node", - "--root", - process.cwd(), - "--output", - outputDir, - ], - env, - ); +export async function runFlueInstrumentationScenario() { + const outputDir = path.join(process.cwd(), ".flue-build", "explicit"); + await runFlueCli([ + "build", + "--target", + "node", + "--root", + process.cwd(), + "--output", + outputDir, + ]); const port = await getFreePort(); const child = spawn(process.execPath, [path.join(outputDir, "server.mjs")], { cwd: process.cwd(), env: { - ...env, + ...process.env, PORT: String(port), }, stdio: ["ignore", "pipe", "pipe"], @@ -96,69 +90,10 @@ export async function runNodeFlueInstrumentationScenario(options) { } } -export async function runCliFlueInstrumentationScenario() { - const flushFile = path.join(process.cwd(), ".flue-build", "cli-flushed"); - await rm(flushFile, { force: true }); - await runFlueCli( - [ - "run", - "instrumentation", - "--target", - "node", - process.env.FLUE_E2E_INPUT_FLAG ?? "--payload", - JSON.stringify(workflowPayload()), - "--root", - process.cwd(), - ], - { - ...scenarioEnv({ autoHook: true, explicitObserve: false }), - FLUE_E2E_FLUSH_FILE: flushFile, - }, - ); - await waitForFile(flushFile); -} - -export function runExplicitFlueInstrumentation() { - return runNodeFlueInstrumentationScenario({ - autoHook: false, - explicitObserve: true, - outputName: "explicit", - }); -} - -export function runAutoFlueInstrumentation() { - return runNodeFlueInstrumentationScenario({ - autoHook: true, - explicitObserve: false, - outputName: "auto-hook", - }); -} - -export function runCliFlueInstrumentation() { - return runCliFlueInstrumentationScenario(); -} - -function scenarioEnv({ autoHook, explicitObserve }) { - const env = { - ...process.env, - FLUE_E2E_EXPLICIT_OBSERVE: explicitObserve ? "1" : "0", - }; - if (!autoHook) { - return env; - } - return { - ...env, - NODE_OPTIONS: [env.NODE_OPTIONS, braintrustHookNodeOption] - .filter(Boolean) - .join(" "), - }; -} - -async function runFlueCli(args, env) { +async function runFlueCli(args) { await new Promise((resolve, reject) => { const child = spawn(process.execPath, [flueCliPath, ...args], { cwd: process.cwd(), - env, stdio: ["ignore", "pipe", "pipe"], }); @@ -245,27 +180,3 @@ async function stopChild(child) { }, 5_000).unref(); }); } - -async function waitForFile(filePath) { - const startedAt = Date.now(); - while (true) { - try { - await access(filePath); - return; - } catch { - if (Date.now() - startedAt > 30_000) { - throw new Error( - `timed out waiting for Flue e2e flush marker: ${filePath}`, - ); - } - await new Promise((resolve) => setTimeout(resolve, 50)); - } - } -} - -export function runMain(main) { - void main().catch((error) => { - console.error(error); - process.exitCode = 1; - }); -} diff --git a/e2e/scenarios/flue-instrumentation/scenario.mjs b/e2e/scenarios/flue-instrumentation/scenario.mjs deleted file mode 100644 index a9bd9d94e..000000000 --- a/e2e/scenarios/flue-instrumentation/scenario.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import "braintrust"; - -await new Promise((resolve) => setImmediate(resolve)); - -const { runAutoFlueInstrumentation, runMain } = - await import("./scenario.impl.mjs"); - -runMain(runAutoFlueInstrumentation); diff --git a/e2e/scenarios/flue-instrumentation/scenario.test.ts b/e2e/scenarios/flue-instrumentation/scenario.test.ts index 73a27bbf7..22d9941be 100644 --- a/e2e/scenarios/flue-instrumentation/scenario.test.ts +++ b/e2e/scenarios/flue-instrumentation/scenario.test.ts @@ -10,49 +10,19 @@ import { defineFlueInstrumentationAssertions } from "./assertions"; import { defineFlueV2InstrumentationAssertions } from "./v2-assertions"; const originalScenarioDir = resolveScenarioDir(import.meta.url); -const generatedScenarioRoot = path.resolve( - originalScenarioDir, - "../../.bt-tmp/generated-scenarios/flue-instrumentation", -); const TIMEOUT_MS = 120_000; -const flueV1ScenarioSourceDir = await prepareGeneratedFlueV1ScenarioSource(); const flueScenarios = await Promise.all([ prepareFlueScenario({ cliPackageName: "@flue/cli", - expectAmbientContext: false, - label: "v0.8 pinned", - runtimePackageName: "@flue/runtime", - sourceDir: originalScenarioDir, - supportsAutoInstrumentation: true, - variantKey: "flue-v0-8-0", - }), - prepareFlueScenario({ - cliPackageName: "flue-cli-v0-8-latest", - expectAmbientContext: false, - label: "v0.8 latest", - modelName: "openai/gpt-5.4-nano", - runtimePackageName: "flue-runtime-v0-8-latest", - sourceDir: originalScenarioDir, - supportsAutoInstrumentation: true, - variantKey: "flue-v0-8-latest", - }), - prepareFlueScenario({ - cliPackageName: "flue-cli-v1", - expectAmbientContext: true, label: "v1 pinned", - runtimePackageName: "flue-runtime-v1", - sourceDir: flueV1ScenarioSourceDir, - supportsAutoInstrumentation: false, + runtimePackageName: "@flue/runtime", variantKey: "flue-v1-0-0-beta-3", }), prepareFlueScenario({ cliPackageName: "flue-cli-v1-latest", - expectAmbientContext: true, label: "v1 latest", modelName: "openai/gpt-5.4-nano", runtimePackageName: "flue-runtime-v1-latest", - sourceDir: flueV1ScenarioSourceDir, - supportsAutoInstrumentation: false, variantKey: "flue-v1-latest", }), ]); @@ -84,7 +54,6 @@ describe.sequential("flue variants", () => { for (const scenario of flueScenarios) { describe.sequential(`flue ${scenario.label} (${scenario.version})`, () => { defineFlueInstrumentationAssertions({ - expectAmbientContext: scenario.expectAmbientContext, name: "explicit instrumentation", runScenario: async ({ runScenarioDir }) => { await runScenarioDir({ @@ -102,54 +71,6 @@ describe.sequential("flue variants", () => { testFileUrl: import.meta.url, timeoutMs: TIMEOUT_MS, }); - - if (scenario.supportsAutoInstrumentation) { - defineFlueInstrumentationAssertions({ - expectAmbientContext: scenario.expectAmbientContext, - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - env: scenario.env, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - originalScenarioDir, - variantKey: scenario.variantKey, - }, - scenarioDir: scenario.scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.variantKey}-auto-hook`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - - defineFlueInstrumentationAssertions({ - expectAmbientContext: scenario.expectAmbientContext, - name: "cli instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.cli.mjs", - env: { - ...scenario.env, - ...(scenario.inputFlag - ? { FLUE_E2E_INPUT_FLAG: scenario.inputFlag } - : {}), - }, - runContext: { - originalScenarioDir, - variantKey: scenario.variantKey, - }, - scenarioDir: scenario.scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.variantKey}-cli`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - } }); } @@ -169,17 +90,13 @@ describe.sequential("flue variants", () => { async function prepareFlueScenario(options: { cliPackageName: string; - expectAmbientContext: boolean; - inputFlag?: string; label: string; modelName?: string; runtimePackageName: string; - sourceDir: string; - supportsAutoInstrumentation: boolean; variantKey: string; }) { const scenarioDir = await prepareScenarioDir({ - scenarioDir: options.sourceDir, + scenarioDir: originalScenarioDir, }); const [cliPackageDir, runtimePackageDir] = await Promise.all([ fs.realpath(path.join(scenarioDir, "node_modules", options.cliPackageName)), @@ -203,7 +120,6 @@ async function prepareFlueScenario(options: { ), ]); return { - ...options, env: { FLUE_CLI_PACKAGE_NAME: options.cliPackageName, FLUE_RUNTIME_PACKAGE_NAME: options.runtimePackageName, @@ -216,39 +132,12 @@ async function prepareFlueScenario(options: { } : {}), }, + label: options.label, scenarioDir, + variantKey: options.variantKey, version: await readInstalledPackageVersion( scenarioDir, options.runtimePackageName, ), }; } - -async function prepareGeneratedFlueV1ScenarioSource() { - const sourceDir = path.join(generatedScenarioRoot, "flue-v1"); - await fs.rm(sourceDir, { force: true, recursive: true }); - await fs.mkdir(sourceDir, { recursive: true }); - await fs.cp(originalScenarioDir, sourceDir, { - filter(source) { - const relative = path.relative(originalScenarioDir, source); - return ( - relative === "" || - !["__cassettes__", "__snapshots__", "node_modules", "versions"].some( - (name) => - relative === name || relative.startsWith(`${name}${path.sep}`), - ) - ); - }, - recursive: true, - }); - await fs.cp(path.join(originalScenarioDir, "versions", "v1"), sourceDir, { - filter(source) { - return ( - source !== - path.join(originalScenarioDir, "versions", "v1", "package.json") - ); - }, - recursive: true, - }); - return sourceDir; -} diff --git a/e2e/scenarios/flue-instrumentation/scenario.ts b/e2e/scenarios/flue-instrumentation/scenario.ts index b96ed4882..a5e57e15d 100644 --- a/e2e/scenarios/flue-instrumentation/scenario.ts +++ b/e2e/scenarios/flue-instrumentation/scenario.ts @@ -1,3 +1,3 @@ -import { runExplicitFlueInstrumentation, runMain } from "./scenario.impl.mjs"; +import { runFlueInstrumentationScenario } from "./scenario.impl.mjs"; -runMain(runExplicitFlueInstrumentation); +await runFlueInstrumentationScenario(); diff --git a/e2e/scenarios/flue-instrumentation/versions/v1/.flue/app.mjs b/e2e/scenarios/flue-instrumentation/versions/v1/.flue/app.mjs deleted file mode 100644 index 6680c7ae8..000000000 --- a/e2e/scenarios/flue-instrumentation/versions/v1/.flue/app.mjs +++ /dev/null @@ -1,112 +0,0 @@ -import { flush, initLogger } from "braintrust"; -import { Hono } from "hono"; -import { mkdir, writeFile } from "node:fs/promises"; -import { dirname } from "node:path"; - -const runtimePackageName = - process.env.FLUE_RUNTIME_PACKAGE_NAME ?? "flue-runtime-v1"; -const [{ instrument, registerProvider }, { flue }] = await Promise.all([ - import(runtimePackageName), - import(`${runtimePackageName}/routing`), -]); - -function projectName() { - const configured = process.env.BRAINTRUST_E2E_PROJECT_NAME; - if (configured) { - return configured; - } - const testRunId = process.env.BRAINTRUST_E2E_RUN_ID ?? "local"; - return `e2e-flue-instrumentation-${testRunId.toLowerCase().replace(/[^a-z0-9-]/g, "-")}`; -} - -initLogger({ projectName: projectName() }); - -const exitProcess = process.exit.bind(process); -if (process.env.FLUE_E2E_FLUSH_FILE) { - let isExiting = false; - process.exit = (code) => { - if (isExiting) { - return exitProcess(code); - } - isExiting = true; - const keepAlive = setTimeout(() => {}, 30_000); - void flushBeforeExit() - .catch((error) => { - console.error(error); - }) - .finally(() => { - clearTimeout(keepAlive); - exitProcess(code); - }); - }; -} - -if (process.env.FLUE_E2E_EXPLICIT_OBSERVE === "1") { - const { braintrustFlueInstrumentation } = await import("braintrust"); - instrument(braintrustFlueInstrumentation()); -} - -const openAIBaseUrl = - process.env.OPENAI_BASE_URL ?? process.env.BRAINTRUST_E2E_MODEL_BASE_URL; -if (openAIBaseUrl) { - registerProvider("openai", { baseUrl: openAIBaseUrl }); -} - -const anthropicBaseUrl = process.env.ANTHROPIC_BASE_URL; -if (anthropicBaseUrl) { - registerProvider("anthropic", { - apiKey: process.env.ANTHROPIC_API_KEY ?? "test-key", - baseUrl: anthropicBaseUrl, - }); -} - -let didScheduleFlush = false; -function scheduleFinalFlush(exitAfterFlush = false) { - if (didScheduleFlush) { - return; - } - didScheduleFlush = true; - const keepAlive = setTimeout(() => {}, 30_000); - void flushBeforeExit() - .catch((error) => { - console.error(error); - }) - .finally(() => { - clearTimeout(keepAlive); - if (exitAfterFlush) { - exitProcess(0); - } - }); -} - -process.on("SIGTERM", () => { - scheduleFinalFlush(true); -}); - -process.on("beforeExit", () => { - scheduleFinalFlush(); -}); - -const app = new Hono(); -app.route("/", flue()); - -async function flushBeforeExit() { - await new Promise((resolve) => setTimeout(resolve, 250)); - await flush(); - if (process.env.FLUE_E2E_FLUSH_FILE) { - await mkdir(dirname(process.env.FLUE_E2E_FLUSH_FILE), { - recursive: true, - }); - await writeFile(process.env.FLUE_E2E_FLUSH_FILE, "ok"); - } -} - -export default { - async fetch(request, env, ctx) { - if (new URL(request.url).pathname === "/__braintrust_flush") { - await flush(); - return new Response("ok"); - } - return app.fetch(request, env, ctx); - }, -}; diff --git a/e2e/scenarios/flue-instrumentation/versions/v1/.flue/workflows/instrumentation.mjs b/e2e/scenarios/flue-instrumentation/versions/v1/.flue/workflows/instrumentation.mjs deleted file mode 100644 index 2c8660430..000000000 --- a/e2e/scenarios/flue-instrumentation/versions/v1/.flue/workflows/instrumentation.mjs +++ /dev/null @@ -1,183 +0,0 @@ -import { traced } from "braintrust"; -import * as v from "valibot"; -import { - FLUE_MODEL, - FLUE_REASONING_MODEL, - SCENARIO_NAME, -} from "../../constants.mjs"; - -const runtimePackageName = - process.env.FLUE_RUNTIME_PACKAGE_NAME ?? "flue-runtime-v1"; -const [{ defineAgent, defineTool, defineWorkflow }, { local }] = - await Promise.all([ - import(runtimePackageName), - import(`${runtimePackageName}/node`), - ]); - -function flueModel() { - return process.env.FLUE_E2E_MODEL ?? FLUE_MODEL; -} - -function flueReasoningModel() { - return process.env.FLUE_E2E_REASONING_MODEL ?? FLUE_REASONING_MODEL; -} - -function fluePromptModel() { - return process.env.FLUE_E2E_PROMPT_MODEL ?? flueReasoningModel(); -} - -function fluePromptThinkingLevel() { - return ( - process.env.FLUE_E2E_PROMPT_THINKING_LEVEL ?? flueReasoningThinkingLevel() - ); -} - -function flueReasoningThinkingLevel() { - return process.env.FLUE_E2E_REASONING_THINKING_LEVEL ?? "medium"; -} - -const flueE2EAgent = defineAgent(() => ({ - compaction: { - keepRecentTokens: 1, - reserveTokens: 64, - }, - cwd: process.cwd(), - instructions: [ - "You are a deterministic Flue instrumentation test agent.", - "Follow user instructions exactly.", - "When asked for a marker, output only that marker and no extra text.", - "When running a local skill file, read it yourself and do not delegate it to a task.", - ].join(" "), - model: flueModel(), - sandbox: local({ cwd: process.cwd() }), - thinkingLevel: "off", -})); - -const lookupTool = defineTool({ - description: - "Return a deterministic lookup result with an id needed by web_search.", - input: v.object({ - query: v.string(), - }), - name: "lookup", - run: async ({ input }) => { - await traced( - async (span) => { - span.log({ output: "lookup-active" }); - }, - { - name: "flue.toolCurrentProbe", - event: { - metadata: { - scenario: SCENARIO_NAME, - }, - }, - }, - ); - - return { - id: "flue-session-2026", - query: input.query, - topic: "session instrumentation", - }; - }, -}); - -const webSearchTool = defineTool({ - description: - "Search a deterministic local web index. Requires the id returned by lookup.", - input: v.object({ - lookupId: v.string(), - query: v.string(), - }), - name: "web_search", - run: async ({ input }) => ({ - lookupId: input.lookupId, - query: input.query, - results: [ - { - title: "Flue reasoning stream instrumentation", - url: "https://example.test/flue/reasoning-streams", - }, - ], - }), -}); - -const summarizeSourceTool = defineTool({ - description: - "Summarize the selected deterministic source after web_search returns a URL.", - input: v.object({ - url: v.string(), - }), - name: "summarize_source", - run: async ({ input }) => ({ - summary: - "Flue emits reasoning, tool execution, and LLM turn events separately.", - url: input.url, - }), -}); - -export async function route(_ctx, next) { - await next(); -} - -export default defineWorkflow({ - agent: flueE2EAgent, - input: v.object({ - scenario: v.optional(v.string()), - metadata: v.optional(v.record(v.string(), v.unknown())), - }), - async run({ harness, input }) { - await traced( - async (span) => { - span.log({ output: "active" }); - }, - { - name: "flue.workflowCurrentProbe", - event: { - metadata: { - scenario: SCENARIO_NAME, - }, - }, - }, - ); - - const session = await harness.session("main"); - const skillSession = await harness.session("skill"); - const taskSession = await harness.session("task"); - - await session.prompt( - [ - "Complete this instrumented research flow.", - "Call exactly one tool per turn and wait for each tool result before choosing the next tool.", - 'Step 1: call lookup with query "flue instrumentation".', - 'Step 2: use the lookup result id as lookupId and call web_search with query "Braintrust Flue reasoning stream instrumentation".', - "Step 3: use the first web_search result url and call summarize_source.", - "After summarize_source returns, reply with exactly PROMPT_DONE and no other text.", - ].join(" "), - { - model: fluePromptModel(), - thinkingLevel: fluePromptThinkingLevel(), - tools: [lookupTool, webSearchTool, summarizeSourceTool], - }, - ); - - await skillSession.skill("e2e-flue-skill", { - args: { marker: "SKILL_DONE" }, - model: flueReasoningModel(), - thinkingLevel: "off", - }); - - await taskSession.task("Reply with exactly TASK_DONE and no other text.", { - model: FLUE_MODEL, - thinkingLevel: "off", - }); - - await session.compact(); - - return { - scenario: input.scenario ?? SCENARIO_NAME, - status: "done", - }; - }, -}); diff --git a/js/src/auto-instrumentations/configs/all.ts b/js/src/auto-instrumentations/configs/all.ts index 23d58969a..3b42cb11a 100644 --- a/js/src/auto-instrumentations/configs/all.ts +++ b/js/src/auto-instrumentations/configs/all.ts @@ -13,7 +13,6 @@ import { cloudflareAgentsConfigs } from "./cloudflare-agents"; import { cloudflareThinkConfigs } from "./cloudflare-think"; import { cohereConfigs } from "./cohere"; import { cursorSDKConfigs } from "./cursor-sdk"; -import { flueConfigs } from "./flue"; import { genkitConfigs } from "./genkit"; import { gitHubCopilotConfigs } from "./github-copilot"; import { googleADKConfigs } from "./google-adk"; @@ -115,10 +114,6 @@ const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] integrations: ["strandsAgentSDK"], configs: strandsAgentSDKConfigs, }, - { - integrations: ["flue"], - configs: flueConfigs, - }, { integrations: ["voyageai"], configs: voyageAIConfigs, diff --git a/js/src/auto-instrumentations/configs/flue.test.ts b/js/src/auto-instrumentations/configs/flue.test.ts deleted file mode 100644 index 456b5110f..000000000 --- a/js/src/auto-instrumentations/configs/flue.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { create } from "@apm-js-collab/code-transformer"; -import { describe, expect, it } from "vitest"; -import { flueConfigs, flueVersionRange } from "./flue"; - -describe("flue auto-instrumentation configs", () => { - it("targets the stable Flue 0.8 context factory", () => { - const matcher = create(flueConfigs); - const flue08Transformer = matcher.getTransformer( - "@flue/runtime", - "0.8.0", - "dist/internal.mjs", - ); - const flue10BetaTransformer = matcher.getTransformer( - "@flue/runtime", - "1.0.0-beta.3", - "dist/internal.mjs", - ); - const flue10Transformer = matcher.getTransformer( - "@flue/runtime", - "1.0.0", - "dist/internal.mjs", - ); - - expect(flueVersionRange).toBe(">=0.8.0 <1.0.0"); - expect(flue08Transformer).toBeDefined(); - expect(flue10BetaTransformer).toBeUndefined(); - expect(flue10Transformer).toBeUndefined(); - const transformed = flue08Transformer!.transform( - ` -function createFlueContext(config) { - return { config }; -} -`, - "esm", - ).code; - - expect(transformed).toContain( - "orchestrion:@flue/runtime:createFlueContext", - ); - }); - - it("does not target Flue content-hashed workflow or tool chunks", () => { - const matcher = create(flueConfigs); - - expect( - matcher.getTransformer( - "@flue/runtime", - "0.8.0", - "dist/handle-agent-DcUclCE2.mjs", - ), - ).toBeUndefined(); - expect( - matcher.getTransformer( - "@flue/runtime", - "0.8.0", - "dist/sandbox-DNEJXjr_.mjs", - ), - ).toBeUndefined(); - expect( - matcher.getTransformer("@flue/runtime", "1.0.0-beta.3", "dist/index.mjs"), - ).toBeUndefined(); - }); -}); diff --git a/js/src/auto-instrumentations/configs/flue.ts b/js/src/auto-instrumentations/configs/flue.ts deleted file mode 100644 index a04c02bf2..000000000 --- a/js/src/auto-instrumentations/configs/flue.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { InstrumentationConfig } from "../orchestrion-js"; -import { flueChannels } from "../../instrumentation/plugins/flue-channels"; - -export const flueVersionRange = ">=0.8.0 <1.0.0"; - -export const flueConfigs: InstrumentationConfig[] = [ - { - channelName: flueChannels.createContext.channelName, - module: { - name: "@flue/runtime", - versionRange: flueVersionRange, - filePath: "dist/internal.mjs", - }, - functionQuery: { - functionName: "createFlueContext", - kind: "Sync", - }, - }, -]; diff --git a/js/src/exports.ts b/js/src/exports.ts index 2f4332842..e8bac9cc2 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -351,8 +351,5 @@ export { // Auto-instrumentation configuration export { configureInstrumentation } from "./instrumentation"; -export { - braintrustFlueObserver, - braintrustFlueInstrumentation, -} from "./instrumentation"; +export { braintrustFlueInstrumentation } from "./instrumentation"; export type { InstrumentationConfig } from "./instrumentation"; diff --git a/js/src/instrumentation/braintrust-plugin.ts b/js/src/instrumentation/braintrust-plugin.ts index 4ed98d53e..53bebc00d 100644 --- a/js/src/instrumentation/braintrust-plugin.ts +++ b/js/src/instrumentation/braintrust-plugin.ts @@ -20,7 +20,6 @@ import { GroqPlugin } from "./plugins/groq-plugin"; import { BedrockRuntimePlugin } from "./plugins/bedrock-runtime-plugin"; import { GenkitPlugin } from "./plugins/genkit-plugin"; import { GitHubCopilotPlugin } from "./plugins/github-copilot-plugin"; -import { FluePlugin } from "./plugins/flue-plugin"; import { LangChainPlugin } from "./plugins/langchain-plugin"; import { LangSmithPlugin } from "./plugins/langsmith-plugin"; import { PiCodingAgentPlugin } from "./plugins/pi-coding-agent-plugin"; @@ -77,7 +76,6 @@ export class BraintrustPlugin extends BasePlugin { private bedrockRuntimePlugin: BedrockRuntimePlugin | null = null; private genkitPlugin: GenkitPlugin | null = null; private gitHubCopilotPlugin: GitHubCopilotPlugin | null = null; - private fluePlugin: FluePlugin | null = null; private langChainPlugin: LangChainPlugin | null = null; private langSmithPlugin: LangSmithPlugin | null = null; private piCodingAgentPlugin: PiCodingAgentPlugin | null = null; @@ -234,11 +232,6 @@ export class BraintrustPlugin extends BasePlugin { this.cloudflareAgentsPlugin.enable(); } - if (integrations.flue !== false) { - this.fluePlugin = new FluePlugin(); - this.fluePlugin.enable(); - } - if (integrations.langchain !== false && integrations.langgraph !== false) { this.langChainPlugin = new LangChainPlugin(); this.langChainPlugin.enable(); @@ -389,11 +382,6 @@ export class BraintrustPlugin extends BasePlugin { this.cloudflareAgentsPlugin = null; } - if (this.fluePlugin) { - this.fluePlugin.disable(); - this.fluePlugin = null; - } - if (this.langChainPlugin) { this.langChainPlugin.disable(); this.langChainPlugin = null; diff --git a/js/src/instrumentation/config.ts b/js/src/instrumentation/config.ts index 35d3d2cb3..e8df49037 100644 --- a/js/src/instrumentation/config.ts +++ b/js/src/instrumentation/config.ts @@ -12,7 +12,6 @@ export interface InstrumentationIntegrationsConfig { cloudflareThink?: boolean; cursor?: boolean; cursorSDK?: boolean; - flue?: boolean; mastra?: boolean; openAIAgents?: boolean; openrouter?: boolean; @@ -80,8 +79,6 @@ const envIntegrationAliases: Record< cursor: "cursor", "cursor-sdk": "cursorSDK", cursorsdk: "cursorSDK", - flue: "flue", - "flue-runtime": "flue", mastra: "mastra", "openai-agents": "openAIAgents", openaiagents: "openAIAgents", @@ -143,7 +140,6 @@ export function getDefaultInstrumentationIntegrations(): Record< cloudflareThink: true, cursor: true, cursorSDK: true, - flue: true, mastra: true, openAIAgents: true, openrouter: true, diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index 338d62abd..ce4352ebc 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -19,10 +19,7 @@ export { BraintrustPlugin } from "./braintrust-plugin"; export type { BraintrustPluginConfig } from "./braintrust-plugin"; export { OpenAIAgentsTraceProcessor } from "./plugins/openai-agents-trace-processor"; export type { OpenAIAgentsTraceProcessorOptions } from "./plugins/openai-agents-trace-processor"; -export { - braintrustFlueInstrumentation, - braintrustFlueObserver, -} from "./plugins/flue-plugin"; +export { braintrustFlueInstrumentation } from "./plugins/flue-plugin"; export { braintrustEveHook, braintrustEveInstrumentation, diff --git a/js/src/instrumentation/plugins/flue-channels.ts b/js/src/instrumentation/plugins/flue-channels.ts deleted file mode 100644 index 308d50aea..000000000 --- a/js/src/instrumentation/plugins/flue-channels.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { channel, defineChannels } from "../core/channel-definitions"; -import { INSTRUMENTATION_NAMES } from "../../span-origin"; -import type { FlueObservableContext } from "../../vendor-sdk-types/flue"; - -export const flueChannels = defineChannels( - "@flue/runtime", - { - createContext: channel<[unknown], FlueObservableContext>({ - channelName: "createFlueContext", - kind: "sync-stream", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.FLUE }, -); diff --git a/js/src/instrumentation/plugins/flue-plugin.test.ts b/js/src/instrumentation/plugins/flue-plugin.test.ts index 071b2a1b4..04901f8cc 100644 --- a/js/src/instrumentation/plugins/flue-plugin.test.ts +++ b/js/src/instrumentation/plugins/flue-plugin.test.ts @@ -37,43 +37,6 @@ vi.mock("../../debug-logger", () => ({ }, })); -const { mockNewTracingChannel, mockTracingChannels } = vi.hoisted(() => { - const tracingChannels = new Map(); - - function tracingChannel(name: string) { - const existing = tracingChannels.get(name); - if (existing) { - return existing; - } - - const handlers = new Set(); - const stores = new Map unknown>(); - const channel = { - __handlers: handlers, - __stores: stores, - start: { - bindStore: vi.fn( - (store: unknown, transform: (message: any) => unknown) => { - stores.set(store, transform); - }, - ), - unbindStore: vi.fn((store: unknown) => stores.delete(store)), - }, - subscribe: vi.fn((handler: any) => { - handlers.add(handler); - }), - unsubscribe: vi.fn((handler: any) => handlers.delete(handler)), - }; - tracingChannels.set(name, channel); - return channel; - } - - return { - mockNewTracingChannel: vi.fn((name: string) => tracingChannel(name)), - mockTracingChannels: tracingChannels, - }; -}); - vi.mock("../../logger", () => ({ BRAINTRUST_CURRENT_SPAN_STORE: mockCurrentSpanStoreSymbol, NOOP_SPAN: {}, @@ -101,24 +64,9 @@ vi.mock("../../logger", () => ({ }, })); -vi.mock("../../isomorph", () => ({ - default: { - newTracingChannel: mockNewTracingChannel, - }, -})); - -import { - FluePlugin, - braintrustFlueInstrumentation, - braintrustFlueObserver, -} from "./flue-plugin"; - -type Subscriber = typeof braintrustFlueObserver; - -const CREATE_CONTEXT_CHANNEL_NAME = - "orchestrion:@flue/runtime:createFlueContext"; +import { braintrustFlueInstrumentation } from "./flue-plugin"; -describe("Flue observe instrumentation", () => { +describe("Flue instrumentation", () => { let spans: Array<{ args: any; end: ReturnType; @@ -160,52 +108,20 @@ describe("Flue observe instrumentation", () => { }); afterEach(() => { - delete (globalThis as Record)[ - Symbol.for("braintrust.flue.auto-state") - ]; delete (globalThis as Record)[ Symbol.for("braintrust.flue.observe-bridge") ]; - for (const channel of mockTracingChannels.values()) { - channel.__handlers.clear(); - channel.__stores.clear(); - } vi.clearAllMocks(); }); - it("exports a subscriber that can be passed directly to Flue observe", () => { - const subscribers: Subscriber[] = []; - const unsubscribe = vi.fn(); - const observe = vi.fn((subscriber: Subscriber) => { - subscribers.push(subscriber); - return unsubscribe; - }); - - const unregister = observe(braintrustFlueObserver); - - expect(observe).toHaveBeenCalledTimes(1); - expect(observe).toHaveBeenCalledWith(braintrustFlueObserver); - expect(subscribers).toHaveLength(1); - - subscribers[0]?.({ - runId: "run-1", - type: "run_start", - workflowName: "research", - }); - expect(findSpan("workflow:research")).toBeDefined(); - - unregister(); - expect(unsubscribe).toHaveBeenCalledTimes(1); - }); - - it("exports a Flue 1.0 instrumentation factory", async () => { + it("exports a Flue instrumentation factory", async () => { const instrumentation = braintrustFlueInstrumentation(); const instrument = vi.fn((value: typeof instrumentation) => value); const registered = instrument(instrumentation); expect(instrument).toHaveBeenCalledWith(instrumentation); - expect(registered.observe).toBe(braintrustFlueObserver); + expect(typeof registered.observe).toBe("function"); expect(registered.key).toBe(Symbol.for("braintrust.flue.instrumentation")); expect(typeof registered.interceptor).toBe("function"); expect(() => registered.dispose()).not.toThrow(); @@ -218,7 +134,7 @@ describe("Flue observe instrumentation", () => { type: "workflow", workflowName: "research", }, - { eventContext: { id: "ctx-1", runId: "run-1" } }, + { eventContext: { id: "ctx-1" } }, async () => mockStartSpan({ name: "app.phase" }), ); const workflowSpan = findSpan("workflow:research"); @@ -232,37 +148,7 @@ describe("Flue observe instrumentation", () => { expect("enterWith" in mockCurrentSpanStore).toBe(false); }); - it("keeps the legacy observer compatible with Flue 1.0 instrumentation", async () => { - expect(braintrustFlueObserver.observe).toBe(braintrustFlueObserver); - expect(braintrustFlueObserver.key).toBe( - Symbol.for("braintrust.flue.instrumentation"), - ); - expect(typeof braintrustFlueObserver.interceptor).toBe("function"); - expect(() => braintrustFlueObserver.dispose()).not.toThrow(); - - const appSpan = await braintrustFlueObserver.interceptor( - { - phase: "start", - runId: "run-1", - startedAt: "2026-05-27T05:12:31.000Z", - type: "workflow", - workflowName: "research", - }, - { eventContext: { id: "ctx-1", runId: "run-1" } }, - async () => mockStartSpan({ name: "app.phase" }), - ); - const workflowSpan = findSpan("workflow:research"); - - expect(workflowSpan).toBeDefined(); - expect(appSpan.spanParents).toEqual([workflowSpan?.spanId]); - expect(mockCurrentSpanStore.run).toHaveBeenCalledWith( - workflowSpan, - expect.any(Function), - ); - expect("enterWith" in mockCurrentSpanStore).toBe(false); - }); - - it("maps Flue 0.8 observe events into semantic Braintrust spans", () => { + it("maps Flue 1 workflow observations into semantic Braintrust spans", () => { const emit = observeEvents(); const usage = flueUsage(); const startedAt = "2026-05-27T05:12:31.000Z"; @@ -270,8 +156,7 @@ describe("Flue observe instrumentation", () => { emit( { instanceId: "instance-1", - owner: { kind: "workflow", workflowName: "research" }, - payload: { + input: { metadata: { scenario: "flue-instrumentation", testRunId: "e2e-run-1", @@ -284,7 +169,7 @@ describe("Flue observe instrumentation", () => { type: "run_start", workflowName: "research", }, - { id: "ctx-1", runId: "run-1" }, + { id: "ctx-1" }, ); emit({ operationId: "op-1", @@ -295,17 +180,19 @@ describe("Flue observe instrumentation", () => { type: "operation_start", }); emit({ - api: "responses", - input: { - messages: [{ content: "Find Flue changes", role: "user" }], - systemPrompt: "Be precise", - tools: [{ name: "lookup", parameters: {} }], - }, - model: "claude-test", operationId: "op-1", - provider: "anthropic", purpose: "agent", - reasoning: "medium", + request: { + api: "responses", + input: { + messages: [{ content: "Find Flue changes", role: "user" }], + systemPrompt: "Be precise", + tools: [{ name: "lookup", parameters: {} }], + }, + providerName: "anthropic", + reasoningLevel: "medium", + requestedModel: "claude-test", + }, runId: "run-1", timestamp: "2026-05-27T05:12:33.000Z", turnId: "turn-1", @@ -331,23 +218,30 @@ describe("Flue observe instrumentation", () => { toolCallId: "tool-1", toolName: "lookup", turnId: "turn-1", - type: "tool_call", + type: "tool", }); emit({ - api: "responses", durationMs: 12, isError: false, - model: "claude-test", operationId: "op-1", - output: { content: [{ text: "done", type: "text" }], role: "assistant" }, - provider: "anthropic", purpose: "agent", + request: { + api: "responses", + providerName: "anthropic", + requestedModel: "claude-test", + }, + response: { + output: { + content: [{ text: "done", type: "text" }], + role: "assistant", + }, + finishReason: "stop", + usage, + }, runId: "run-1", - stopReason: "stop", timestamp: "2026-05-27T05:12:36.000Z", turnId: "turn-1", type: "turn", - usage, }); emit({ agent: "worker", @@ -493,150 +387,6 @@ describe("Flue observe instrumentation", () => { expect(mockFlush).toHaveBeenCalledTimes(1); }); - it("maps Flue 1.0 observations into semantic Braintrust spans", () => { - const emit = observeEvents(); - const usage = flueUsage(); - - emit( - { - eventIndex: 0, - input: { - metadata: { scenario: "flue-v1" }, - topic: "native instrumentation", - }, - runId: "run-1", - timestamp: "2026-05-27T05:12:31.000Z", - type: "run_start", - v: 3, - workflowName: "research", - }, - { id: "ctx-1", runId: "run-1" }, - ); - emit({ - eventIndex: 1, - operationId: "op-1", - operationKind: "prompt", - runId: "run-1", - type: "operation_start", - v: 3, - }); - emit({ - eventIndex: 2, - operationId: "op-1", - purpose: "agent", - request: { - api: "responses", - input: { - messages: [{ content: "Find native hooks", role: "user" }], - systemPrompt: "Be exact", - tools: [{ name: "lookup" }], - }, - model: "claude-test", - providerId: "anthropic", - providerName: "anthropic", - reasoning: "medium", - }, - runId: "run-1", - turnId: "turn-1", - type: "turn_request", - v: 3, - }); - emit({ - args: { query: "native flue instrumentation" }, - eventIndex: 3, - operationId: "op-1", - runId: "run-1", - toolCallId: "tool-1", - toolName: "lookup", - turnId: "turn-1", - type: "tool_start", - v: 3, - }); - emit({ - durationMs: 4, - eventIndex: 4, - isError: false, - operationId: "op-1", - output: { ok: true }, - runId: "run-1", - toolCallId: "tool-1", - toolName: "lookup", - turnId: "turn-1", - type: "tool", - v: 3, - }); - emit({ - durationMs: 12, - eventIndex: 5, - isError: false, - operationId: "op-1", - purpose: "agent", - request: { - api: "responses", - model: "claude-test", - providerId: "anthropic", - providerName: "anthropic", - }, - response: { - output: { content: [{ text: "done", type: "text" }] }, - stopReason: "stop", - usage, - }, - runId: "run-1", - turnId: "turn-1", - type: "turn", - v: 3, - }); - emit({ - durationMs: 50, - eventIndex: 6, - isError: false, - operationId: "op-1", - operationKind: "prompt", - result: { text: "PROMPT_DONE", usage }, - runId: "run-1", - type: "operation", - usage, - v: 3, - }); - - const workflowSpan = findSpan("workflow:research"); - const turnSpan = findSpan("flue.turn"); - const toolSpan = findSpan("tool:lookup"); - const operationSpan = findSpan("flue.prompt"); - - expect(workflowSpan?.args.event.input).toMatchObject({ - metadata: { scenario: "flue-v1" }, - topic: "native instrumentation", - }); - expect(turnSpan?.args.event).toMatchObject({ - input: [{ content: "Find native hooks", role: "user" }], - metadata: { - "flue.api": "responses", - "flue.model": "claude-test", - "flue.provider": "anthropic", - "flue.system_prompt": "Be exact", - provider: "anthropic", - reasoning: "medium", - tools: [{ name: "lookup" }], - }, - }); - expect(toolSpan?.log).toHaveBeenCalledWith( - expect.objectContaining({ output: { ok: true } }), - ); - expect(turnSpan?.log).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: expect.objectContaining({ - "flue.stop_reason": "stop", - }), - output: { content: [{ text: "done", type: "text" }] }, - }), - ); - expect(operationSpan?.log).toHaveBeenCalledWith( - expect.objectContaining({ output: "PROMPT_DONE" }), - ); - }); - it("maps released Flue 2.0 observations and retains the final agent output", () => { const emit = observeEvents(); const usage = flueUsage(); @@ -790,59 +540,65 @@ describe("Flue observe instrumentation", () => { type: "operation_start", }); emit({ - input: { - messages: [previousUser, previousAssistant, currentUser], - systemPrompt: "Be precise", - tools: [{ name: "lookup" }], - }, operationId: "op-1", purpose: "agent", + request: { + input: { + messages: [previousUser, previousAssistant, currentUser], + systemPrompt: "Be precise", + tools: [{ name: "lookup" }], + }, + }, runId: "run-1", turnId: "turn-1", type: "turn_request", }); emit({ operationId: "op-1", - output: toolCall, purpose: "agent", + response: { output: toolCall }, runId: "run-1", turnId: "turn-1", type: "turn", }); emit({ - input: { - messages: [ - previousUser, - previousAssistant, - currentUser, - toolCall, - toolResult, - ], - systemPrompt: "Be precise", - tools: [{ name: "lookup" }], - }, operationId: "op-1", purpose: "agent", + request: { + input: { + messages: [ + previousUser, + previousAssistant, + currentUser, + toolCall, + toolResult, + ], + systemPrompt: "Be precise", + tools: [{ name: "lookup" }], + }, + }, runId: "run-1", turnId: "turn-1", type: "turn_request", }); emit({ operationId: "op-1", - output: { content: "done", role: "assistant" }, purpose: "agent", + response: { output: { content: "done", role: "assistant" } }, runId: "run-1", turnId: "turn-1", type: "turn", }); emit({ - input: { - messages: [currentUser], - systemPrompt: "Use compacted context", - tools: [{ name: "search" }], - }, operationId: "op-1", purpose: "agent", + request: { + input: { + messages: [currentUser], + systemPrompt: "Use compacted context", + tools: [{ name: "search" }], + }, + }, runId: "run-1", turnId: "turn-1", type: "turn_request", @@ -927,13 +683,15 @@ describe("Flue observe instrumentation", () => { type: "operation_start", }); emit({ - input: { - messages: [{ content: "finish", role: "user" }], - }, - model: "claude-test", operationId: "op-1", - provider: "anthropic", purpose: "agent", + request: { + input: { + messages: [{ content: "finish", role: "user" }], + }, + providerName: "anthropic", + requestedModel: "claude-test", + }, runId: "run-1", turnId: "turn-1", type: "turn_request", @@ -1008,13 +766,15 @@ describe("Flue observe instrumentation", () => { type: "compaction_start", }); emit({ - input: { - messages: [{ content: "summarize", role: "user" }], - }, - model: "gpt-test", operationId: "op-compact", - provider: "openai", purpose: "compaction_prefix", + request: { + input: { + messages: [{ content: "summarize", role: "user" }], + }, + providerName: "openai", + requestedModel: "gpt-test", + }, runId: "run-1", session: "main", turnId: "turn-compact", @@ -1220,7 +980,7 @@ describe("Flue observe instrumentation", () => { runId: "run-1", toolCallId: "tool-1", toolName: "lookup", - type: "tool_call", + type: "tool", }); expect(mockCurrentParentSpan.current).toBeUndefined(); @@ -1236,10 +996,10 @@ describe("Flue observe instrumentation", () => { type: "run_start", workflowName: "research", }, - { id: "ctx-1", runId: "run-1" }, + { id: "ctx-1" }, ); const workflowSpan = findSpan("workflow:research"); - const appSpan = await braintrustFlueObserver.interceptor( + const appSpan = await braintrustFlueInstrumentation().interceptor( { phase: "start", runId: "run-1", @@ -1247,7 +1007,7 @@ describe("Flue observe instrumentation", () => { type: "workflow", workflowName: "research", }, - { eventContext: { id: "ctx-1", runId: "run-1" } }, + { eventContext: { id: "ctx-1" } }, async () => mockStartSpan({ name: "app.phase" }), ); @@ -1283,7 +1043,7 @@ describe("Flue observe instrumentation", () => { }); const toolSpan = findSpan("tool:lookup"); - const appSpan = await braintrustFlueObserver.interceptor( + const appSpan = await braintrustFlueInstrumentation().interceptor( { toolCallId: "tool-1", toolName: "lookup", type: "tool" }, { operationId: "op-prompt", runId: "run-1" }, async () => mockStartSpan({ name: "app.tool-phase" }), @@ -1312,11 +1072,13 @@ describe("Flue observe instrumentation", () => { type: "operation_start", }); emit({ - input: { messages: [{ content: "hello", role: "user" }] }, - model: "claude-test", operationId: "op-prompt", - provider: "anthropic", purpose: "agent", + request: { + input: { messages: [{ content: "hello", role: "user" }] }, + providerName: "anthropic", + requestedModel: "claude-test", + }, runId: "run-1", turnId: "turn-1", type: "turn_request", @@ -1332,17 +1094,17 @@ describe("Flue observe instrumentation", () => { const operationSpan = findSpan("flue.prompt"); const turnSpan = findSpan("flue.turn"); const taskSpan = findSpan("flue.task"); - const agentAppSpan = await braintrustFlueObserver.interceptor( + const agentAppSpan = await braintrustFlueInstrumentation().interceptor( { operationId: "op-prompt", operationKind: "prompt", type: "agent" }, { operationId: "op-prompt", runId: "run-1" }, async () => mockStartSpan({ name: "app.agent-phase" }), ); - const modelAppSpan = await braintrustFlueObserver.interceptor( + const modelAppSpan = await braintrustFlueInstrumentation().interceptor( { turnId: "turn-1", type: "model" }, { operationId: "op-prompt", runId: "run-1", turnId: "turn-1" }, async () => mockStartSpan({ name: "app.model-phase" }), ); - const taskAppSpan = await braintrustFlueObserver.interceptor( + const taskAppSpan = await braintrustFlueInstrumentation().interceptor( { taskId: "task-1", type: "task" }, { operationId: "op-prompt", runId: "run-1", taskId: "task-1" }, async () => mockStartSpan({ name: "app.task-phase" }), @@ -1354,149 +1116,9 @@ describe("Flue observe instrumentation", () => { expect(mockCurrentParentSpan.current).toBeUndefined(); }); - it("subscribes transformed Flue contexts for auto instrumentation", () => { - const plugin = new FluePlugin(); - const contextSubscribers: Array<(event: unknown) => unknown> = []; - const unsubscribeContext = vi.fn(); - const context = { - id: "ctx-1", - runId: "run-1", - subscribeEvent: vi.fn((subscriber: (event: unknown) => unknown) => { - contextSubscribers.push(subscriber); - return unsubscribeContext; - }), - }; - - plugin.enable(); - expect(mockNewTracingChannel).toHaveBeenCalledWith( - CREATE_CONTEXT_CHANNEL_NAME, - ); - expect( - tracingChannel(CREATE_CONTEXT_CHANNEL_NAME).subscribe, - ).toHaveBeenCalledTimes(1); - - emitCreateContextEnd(context); - - expect(context.subscribeEvent).toHaveBeenCalledTimes(1); - contextSubscribers[0]?.({ - runId: "run-1", - type: "run_start", - workflowName: "auto-research", - }); - - expect( - findSpan("workflow:auto-research")?.args.event.metadata, - ).toMatchObject({ - "flue.context_id": "ctx-1", - "flue.context_run_id": "run-1", - }); - contextSubscribers[0]?.({ - durationMs: 1, - isError: false, - result: "done", - runId: "run-1", - type: "run_end", - }); - expect(unsubscribeContext).toHaveBeenCalledTimes(1); - - plugin.disable(); - - expect( - tracingChannel(CREATE_CONTEXT_CHANNEL_NAME).unsubscribe, - ).toHaveBeenCalledTimes(1); - expect(unsubscribeContext).toHaveBeenCalledTimes(1); - }); - - it("keeps auto instrumentation idempotent across plugin instances", () => { - const first = new FluePlugin(); - const second = new FluePlugin(); - const contextSubscribers: Array<(event: unknown) => unknown> = []; - const unsubscribeContext = vi.fn(); - const context = { - subscribeEvent: vi.fn((subscriber: (event: unknown) => unknown) => { - contextSubscribers.push(subscriber); - return unsubscribeContext; - }), - }; - - first.enable(); - second.enable(); - emitCreateContextEnd(context); - emitCreateContextEnd(context); - - expect( - tracingChannel(CREATE_CONTEXT_CHANNEL_NAME).subscribe, - ).toHaveBeenCalledTimes(1); - expect(context.subscribeEvent).toHaveBeenCalledTimes(1); - - first.disable(); - expect( - tracingChannel(CREATE_CONTEXT_CHANNEL_NAME).unsubscribe, - ).not.toHaveBeenCalled(); - expect(unsubscribeContext).not.toHaveBeenCalled(); - - second.disable(); - expect( - tracingChannel(CREATE_CONTEXT_CHANNEL_NAME).unsubscribe, - ).toHaveBeenCalledTimes(1); - - contextSubscribers[0]?.({ - runId: "run-after-disable", - type: "run_start", - workflowName: "after-disable", - }); - expect(unsubscribeContext).toHaveBeenCalledTimes(1); - expect(findSpan("workflow:after-disable")).toBeUndefined(); - }); - - it("unsubscribes direct Flue contexts on terminal operation events", () => { - const plugin = new FluePlugin(); - const contextSubscribers: Array<(event: unknown) => unknown> = []; - const unsubscribeContext = vi.fn(); - const context = { - id: "direct-agent-1", - subscribeEvent: vi.fn((subscriber: (event: unknown) => unknown) => { - contextSubscribers.push(subscriber); - return unsubscribeContext; - }), - }; - - plugin.enable(); - emitCreateContextEnd(context); - contextSubscribers[0]?.({ - durationMs: 2, - instanceId: "direct-agent-1", - isError: false, - operationId: "op-1", - operationKind: "prompt", - result: "done", - type: "operation", - }); - - expect(unsubscribeContext).toHaveBeenCalledTimes(1); - expect(findSpan("flue.prompt")).toBeDefined(); - - plugin.disable(); - }); - function observeEvents() { return (event: unknown, ctx?: unknown) => - braintrustFlueObserver(event, ctx); - } - - function emitCreateContextEnd(result: unknown) { - for (const handlers of tracingChannel(CREATE_CONTEXT_CHANNEL_NAME) - .__handlers) { - handlers.end?.({ result }); - } - } - - function tracingChannel(channelName: string) { - const channel = mockTracingChannels.get(channelName); - if (!channel) { - throw new Error(`Missing mocked tracing channel: ${channelName}`); - } - return channel; + braintrustFlueInstrumentation().observe(event, ctx); } function findSpan(name: string) { diff --git a/js/src/instrumentation/plugins/flue-plugin.ts b/js/src/instrumentation/plugins/flue-plugin.ts index 81de77992..1426afde6 100644 --- a/js/src/instrumentation/plugins/flue-plugin.ts +++ b/js/src/instrumentation/plugins/flue-plugin.ts @@ -1,7 +1,5 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import { debugLogger } from "../../debug-logger"; -import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers } from "../../isomorph"; import { BRAINTRUST_CURRENT_SPAN_STORE, NOOP_SPAN, @@ -11,13 +9,11 @@ import { withCurrent, } from "../../logger"; import type { Span, StartSpanArgs } from "../../logger"; -import type { CurrentSpanStore } from "../../logger"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, } from "../../span-origin"; import { SpanTypeAttribute } from "../../../util/index"; -import { flueChannels } from "./flue-channels"; import type { FlueBaseEvent, FlueCompactionEvent, @@ -28,7 +24,6 @@ import type { FlueExecutionInterceptor, FlueExecutionOperation, FlueInstrumentation, - FlueObservableContext, FlueOperationEvent, FlueOperationKind, FlueOperationStartEvent, @@ -44,20 +39,6 @@ import type { FlueTurnRequestEvent, } from "../../vendor-sdk-types/flue"; -type FlueObserver = (event: unknown, ctx?: unknown) => void; -type BraintrustFlueObserver = FlueObserver & FlueInstrumentation; - -type FlueAutoState = { - createContextChannel?: ReturnType< - typeof flueChannels.createContext.tracingChannel - >; - createContextHandlers?: IsoChannelHandlers< - ChannelMessage - >; - contexts: WeakSet; - refCount: number; -}; - type SpanState = { latestAgentOutput?: unknown; loggedInput?: boolean; @@ -73,7 +54,6 @@ type FlueTurnInputState = { toolsFingerprint?: string; }; -const FLUE_AUTO_STATE = Symbol.for("braintrust.flue.auto-state"); const FLUE_INSTRUMENTATION_KEY = Symbol.for("braintrust.flue.instrumentation"); const FLUE_OBSERVE_BRIDGE = Symbol.for("braintrust.flue.observe-bridge"); @@ -88,9 +68,9 @@ const interceptFlueExecution: FlueExecutionInterceptor = ( ) => getObserveBridge().intercept(operation, ctx, next); /** - * Manual instrumentation for flue. + * Instrumentation for flue. * - * This should be passed to flue's `instrument()` API if not using auto-instrumentation: `instrument(braintrustFlueInstrumentation())` + * Pass this to flue's `instrument()` API: `instrument(braintrustFlueInstrumentation())` */ export function braintrustFlueInstrumentation(): FlueInstrumentation { return { @@ -101,71 +81,6 @@ export function braintrustFlueInstrumentation(): FlueInstrumentation { }; } -/** - * Observer for flue pre version 1.0.0. - * - * This observer should be passed to flue's `observe()` API if not using auto-instrumentation. - */ -export const braintrustFlueObserver: BraintrustFlueObserver = Object.assign( - observeFlue, - braintrustFlueInstrumentation(), -); - -export class FluePlugin extends BasePlugin { - protected onEnable(): void { - this.unsubscribers.push(enableFlueAutoInstrumentation()); - } - - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - } -} - -function enableFlueAutoInstrumentation(): () => void { - const state = getAutoState(); - state.refCount += 1; - - if (!state.createContextHandlers) { - const createContextChannel = flueChannels.createContext.tracingChannel(); - const handlers: IsoChannelHandlers< - ChannelMessage - > = { - end: (event) => { - subscribeToFlueContext(event.result, state); - }, - }; - - createContextChannel.subscribe(handlers); - state.createContextChannel = createContextChannel; - state.createContextHandlers = handlers; - } - - let released = false; - return () => { - if (released) { - return; - } - released = true; - releaseAutoState(state); - }; -} - -function getAutoState(): FlueAutoState { - const existing = Reflect.get(globalThis, FLUE_AUTO_STATE); - if (isAutoState(existing)) { - return existing; - } - const state: FlueAutoState = { - contexts: new WeakSet(), - refCount: 0, - }; - Reflect.set(globalThis, FLUE_AUTO_STATE, state); - return state; -} - function getObserveBridge(): FlueObserveBridge { const existing = Reflect.get(globalThis, FLUE_OBSERVE_BRIDGE); if (isFlueObserveBridge(existing)) { @@ -180,94 +95,7 @@ function isFlueObserveBridge(value: unknown): value is FlueObserveBridge { return ( isObjectLike(value) && typeof Reflect.get(value, "handle") === "function" && - typeof Reflect.get(value, "reset") === "function" - ); -} - -function isAutoState(value: unknown): value is FlueAutoState { - return ( - isObjectLike(value) && - Reflect.get(value, "contexts") instanceof WeakSet && - typeof Reflect.get(value, "refCount") === "number" - ); -} - -function releaseAutoState(state: FlueAutoState): void { - state.refCount -= 1; - if (state.refCount > 0) { - return; - } - - try { - if (state.createContextChannel && state.createContextHandlers) { - state.createContextChannel.unsubscribe(state.createContextHandlers); - } - } finally { - Reflect.deleteProperty(globalThis, FLUE_AUTO_STATE); - } -} - -function subscribeToFlueContext(value: unknown, state: FlueAutoState): void { - if (!isObservableFlueContext(value) || state.contexts.has(value)) { - return; - } - - const ctx = flueContextFromUnknown(value); - let released = false; - let unsubscribe: (() => void) | undefined; - const release = () => { - if (released) { - return; - } - released = true; - try { - unsubscribe?.(); - } catch (error) { - logInstrumentationError("Flue context unsubscribe", error); - } - }; - - try { - unsubscribe = value.subscribeEvent((event) => { - if (state.refCount <= 0) { - release(); - return; - } - - braintrustFlueObserver(event, ctx); - if (isAutoContextTerminalEvent(event, ctx)) { - release(); - } - }); - state.contexts.add(value); - } catch (error) { - logInstrumentationError("Flue context subscription", error); - } -} - -function isAutoContextTerminalEvent( - event: unknown, - ctx: FlueContext | undefined, -): boolean { - if (!isObjectLike(event)) { - return false; - } - const type = Reflect.get(event, "type"); - if (type === "run_end") { - return true; - } - if (type !== "operation") { - return false; - } - return !ctx?.runId && typeof Reflect.get(event, "runId") !== "string"; -} - -function isObservableFlueContext( - value: unknown, -): value is FlueObservableContext { - return ( - isObjectLike(value) && - typeof Reflect.get(value, "subscribeEvent") === "function" + typeof Reflect.get(value, "intercept") === "function" ); } @@ -282,7 +110,6 @@ function isFlueEvent(event: object): event is FlueEvent { type === "turn_request" || type === "turn" || type === "tool_start" || - type === "tool_call" || type === "tool" || type === "task_start" || type === "task" || @@ -296,11 +123,7 @@ function flueContextFromUnknown(ctx: unknown): FlueContext | undefined { return undefined; } const id = Reflect.get(ctx, "id"); - const runId = Reflect.get(ctx, "runId"); - return { - ...(typeof id === "string" ? { id } : {}), - ...(typeof runId === "string" ? { runId } : {}), - }; + return typeof id === "string" ? { id } : undefined; } function isObjectLike(value: unknown): value is object { @@ -332,16 +155,6 @@ class FlueObserveBridge { } } - reset(): void { - this.compactionsByKey.clear(); - this.operationsById.clear(); - this.runsById.clear(); - this.seenEvents = new WeakSet(); - this.tasksById.clear(); - this.toolsByKey.clear(); - this.turnsByKey.clear(); - } - intercept( operation: FlueExecutionOperation, executionContext: FlueExecutionContext, @@ -389,7 +202,6 @@ class FlueObserveBridge { flueContextFromUnknown(executionContext.eventContext) ?? flueContextFromUnknown({ id: operation.workflowName, - runId: operation.runId, }); if (operation.phase === "resume") { this.handleRunResume( @@ -399,7 +211,6 @@ class FlueObserveBridge { startedAt: operation.startedAt, timestamp: new Date().toISOString(), type: "run_resume", - v: 3, workflowName: operation.workflowName, }, ctx, @@ -413,7 +224,6 @@ class FlueObserveBridge { startedAt: operation.startedAt, timestamp: new Date().toISOString(), type: "run_start", - v: 3, workflowName: operation.workflowName, }, ctx, @@ -472,7 +282,6 @@ class FlueObserveBridge { case "tool_start": this.handleToolStart(event); return; - case "tool_call": case "tool": this.handleToolCall(event); return; @@ -501,15 +310,12 @@ class FlueObserveBridge { return; } - const workflowName = - event.workflowName ?? - event.owner?.workflowName ?? - (typeof ctx?.id === "string" ? ctx.id : "unknown"); - const input = flueRunInput(event); + const workflowName = event.workflowName ?? ctx?.id ?? "unknown"; + const input = event.input; const metadata = { ...extractPayloadMetadata(input), ...extractEventMetadata(event, ctx), - ...(workflowName ? { "flue.workflow_name": workflowName } : {}), + "flue.workflow_name": workflowName, provider: "flue", }; const existing = this.runsById.get(event.runId); @@ -543,11 +349,10 @@ class FlueObserveBridge { return; } - const workflowName = - event.workflowName ?? (typeof ctx?.id === "string" ? ctx.id : "unknown"); + const workflowName = event.workflowName ?? ctx?.id ?? "unknown"; const metadata = { ...extractEventMetadata(event, ctx), - ...(workflowName ? { "flue.workflow_name": workflowName } : {}), + "flue.workflow_name": workflowName, "flue.workflow_phase": "resume", provider: "flue", }; @@ -664,20 +469,20 @@ class FlueObserveBridge { } private handleTurnRequest(event: FlueTurnRequestEvent): void { - const key = turnKey(event); + const key = event.turnId; if (!key) { return; } - const input = flueTurnRequestInput(event); + const input = event.request?.input; const operation = event.operationId ? this.operationsById.get(event.operationId) : undefined; const turnInput = prepareFlueTurnInput(event, input, operation); - const model = flueTurnRequestModel(event); - const provider = flueTurnRequestProvider(event); - const api = flueTurnRequestApi(event); - const reasoning = flueTurnRequestReasoning(event); + const model = event.request?.requestedModel; + const provider = event.request?.providerName ?? event.request?.providerId; + const api = event.request?.api; + const reasoning = event.request?.reasoningLevel; const metadata = { ...extractEventMetadata(event), ...(api ? { "flue.api": api } : {}), @@ -707,19 +512,20 @@ class FlueObserveBridge { } private handleTurn(event: FlueTurnEvent): void { - const key = turnKey(event); + const key = event.turnId; if (!key) { return; } const state = this.turnsByKey.get(key) ?? this.startSyntheticTurn(event); - const model = flueTurnModel(event); - const provider = flueTurnProvider(event); - const api = flueTurnApi(event); - const stopReason = flueTurnStopReason(event); - const usage = flueTurnUsage(event); - const output = flueTurnOutput(event); - const error = flueTurnError(event); + const model = + event.response?.responseModel ?? event.request?.requestedModel; + const provider = event.request?.providerName ?? event.request?.providerId; + const api = event.request?.api; + const stopReason = event.response?.finishReason; + const usage = event.response?.usage; + const output = event.response?.output; + const error = event.response?.error; const metadata = { ...state.metadata, ...extractEventMetadata(event), @@ -758,7 +564,7 @@ class FlueObserveBridge { return; } - const input = flueToolInput(event); + const input = event.args; const metadata = { ...extractEventMetadata(event), ...(event.toolName ? { "flue.tool_name": event.toolName } : {}), @@ -933,7 +739,7 @@ class FlueObserveBridge { } private parentSpanForEvent(event: FlueBaseEvent): Span | undefined { - const turn = turnKey(event); + const turn = event.turnId; if (turn) { const turnState = this.turnsByKey.get(turn); if (turnState) { @@ -1017,9 +823,10 @@ class FlueObserveBridge { } private startSyntheticTurn(event: FlueTurnEvent): SpanState { - const model = flueTurnModel(event); - const provider = flueTurnProvider(event); - const api = flueTurnApi(event); + const model = + event.response?.responseModel ?? event.request?.requestedModel; + const provider = event.request?.providerName ?? event.request?.providerId; + const api = event.request?.api; const metadata = { ...extractEventMetadata(event), ...(api ? { "flue.api": api } : {}), @@ -1208,10 +1015,7 @@ function extractEventMetadata( ...(event.taskId ? { "flue.task_id": event.taskId } : {}), ...(event.operationId ? { "flue.operation_id": event.operationId } : {}), ...(event.turnId ? { "flue.turn_id": event.turnId } : {}), - ...(typeof ctx?.id === "string" ? { "flue.context_id": ctx.id } : {}), - ...(typeof ctx?.runId === "string" - ? { "flue.context_run_id": ctx.runId } - : {}), + ...(ctx ? { "flue.context_id": ctx.id } : {}), }; } @@ -1226,19 +1030,9 @@ function extractPayloadMetadata(payload: unknown): Record { return Object.fromEntries(Object.entries(metadata)); } -function flueRunInput(event: FlueRunStartEvent): unknown { - return event.input !== undefined ? event.input : event.payload; -} - -function flueTurnRequestInput( - event: FlueTurnRequestEvent, -): FlueTurnRequestEvent["input"] { - return event.request?.input ?? event.input; -} - function prepareFlueTurnInput( event: FlueTurnRequestEvent, - input: FlueTurnRequestEvent["input"], + input: NonNullable["input"], operation: SpanState | undefined, ): { messages: unknown[] | undefined; metadata: Record } { const messages = input?.messages; @@ -1347,90 +1141,15 @@ function flueOperationInput(event: FlueOperationEvent): unknown { : undefined; } -function flueTurnRequestModel(event: FlueTurnRequestEvent): string | undefined { - return event.request?.requestedModel ?? event.request?.model ?? event.model; -} - -function flueTurnRequestProvider( - event: FlueTurnRequestEvent, -): string | undefined { - return ( - event.request?.providerName ?? event.provider ?? event.request?.providerId - ); -} - -function flueTurnRequestApi(event: FlueTurnRequestEvent): string | undefined { - return event.request?.api ?? event.api; -} - -function flueTurnRequestReasoning( - event: FlueTurnRequestEvent, -): string | undefined { - return ( - event.request?.reasoningLevel ?? event.request?.reasoning ?? event.reasoning - ); -} - -function flueTurnModel(event: FlueTurnEvent): string | undefined { - return ( - event.response?.responseModel ?? - event.request?.requestedModel ?? - event.request?.model ?? - event.model - ); -} - -function flueTurnProvider(event: FlueTurnEvent): string | undefined { - return ( - event.request?.providerName ?? event.provider ?? event.request?.providerId - ); -} - -function flueTurnApi(event: FlueTurnEvent): string | undefined { - return event.request?.api ?? event.api; -} - -function flueTurnUsage(event: FlueTurnEvent): unknown { - return event.response?.usage ?? event.usage; -} - -function flueTurnOutput(event: FlueTurnEvent): unknown { - return event.response?.output ?? event.output; -} - -function flueTurnStopReason(event: FlueTurnEvent): string | undefined { - return ( - event.response?.finishReason ?? - event.response?.stopReason ?? - event.stopReason - ); -} - -function flueTurnError(event: FlueTurnEvent): unknown { - return ( - event.response?.error ?? event.response?.errorInfo?.message ?? event.error - ); -} - -function flueToolInput(event: FlueToolStartEvent): unknown { - if (event.args !== undefined) { - return event.args; - } - if (event.arguments !== undefined) { - return event.arguments; - } - return event.input; -} - function flueToolOutput(event: FlueToolCallEvent): unknown { if (Object.hasOwn(event, "effectiveResult")) { return event.effectiveResult; } - return event.output !== undefined ? event.output : event.result; + return event.result; } function flueToolError(event: FlueToolCallEvent): unknown { - return event.error ?? event.errorInfo?.message ?? flueToolOutput(event); + return event.errorInfo?.message ?? flueToolOutput(event); } function operationOutput(event: FlueOperationEvent): unknown { @@ -1529,10 +1248,6 @@ function eventTime(value: unknown): number | undefined { return Number.isFinite(timestamp) ? timestamp / 1000 : undefined; } -function turnKey(event: FlueBaseEvent): string | undefined { - return event.turnId; -} - function toolKey(event: FlueBaseEvent & { toolCallId?: string }): string { return `${event.turnId ?? event.operationId ?? event.taskId ?? event.runId ?? "unknown"}:${event.toolCallId ?? "unknown"}`; } @@ -1594,11 +1309,7 @@ function runWithCurrentSpanStore( const state = _internalGetGlobalState(); const contextManager = state?.contextManager; const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] + ? Reflect.get(contextManager, BRAINTRUST_CURRENT_SPAN_STORE) : undefined; if (contextManager && typeof currentSpanStore?.run === "function") { diff --git a/js/src/instrumentation/plugins/instrumentation-names.test.ts b/js/src/instrumentation/plugins/instrumentation-names.test.ts index d8f4418b8..5b0585839 100644 --- a/js/src/instrumentation/plugins/instrumentation-names.test.ts +++ b/js/src/instrumentation/plugins/instrumentation-names.test.ts @@ -14,7 +14,6 @@ import { cloudflareAgentsChannels } from "./cloudflare-agents-channels"; import { cloudflareThinkChannels } from "./cloudflare-think-channels"; import { cohereChannels } from "./cohere-channels"; import { cursorSDKChannels } from "./cursor-sdk-channels"; -import { flueChannels } from "./flue-channels"; import { genkitChannels, genkitCoreChannels } from "./genkit-channels"; import { gitHubCopilotChannels } from "./github-copilot-channels"; import { googleADKChannels } from "./google-adk-channels"; @@ -56,7 +55,6 @@ describe("built-in instrumentation provenance names", () => { ], [cohereChannels.chat, INSTRUMENTATION_NAMES.COHERE], [cursorSDKChannels.create, INSTRUMENTATION_NAMES.CURSOR_SDK], - [flueChannels.createContext, INSTRUMENTATION_NAMES.FLUE], [genkitChannels.generate, INSTRUMENTATION_NAMES.GENKIT], [genkitCoreChannels.actionSpan, INSTRUMENTATION_NAMES.GENKIT], [gitHubCopilotChannels.createSession, INSTRUMENTATION_NAMES.GITHUB_COPILOT], diff --git a/js/src/vendor-sdk-types/flue.ts b/js/src/vendor-sdk-types/flue.ts index 367046fe2..2f7c75222 100644 --- a/js/src/vendor-sdk-types/flue.ts +++ b/js/src/vendor-sdk-types/flue.ts @@ -1,5 +1,5 @@ /** - * Vendored types for @flue/runtime observe/instrument-based instrumentation. + * Vendored types for @flue/runtime instrumentation. * * Keep this surface intentionally narrow. These types are not exported to SDK * users and should only cover fields we read, correlate, or log. @@ -16,17 +16,11 @@ export interface FlueUsage { cacheWrite?: number; totalTokens?: number; cost?: { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; total?: number; }; } export interface FlueBaseEvent { - type?: string; - v?: number; runId?: string; instanceId?: string; submissionId?: string; @@ -41,7 +35,6 @@ export interface FlueBaseEvent { harness?: string; operationId?: string; turnId?: string; - [key: string]: unknown; } export interface FlueRunStartEvent extends FlueBaseEvent { @@ -49,13 +42,7 @@ export interface FlueRunStartEvent extends FlueBaseEvent { runId: string; startedAt?: string; workflowName?: string; - owner?: { - kind?: string; - workflowName?: string; - instanceId?: string; - }; input?: unknown; - payload?: unknown; } export interface FlueRunResumeEvent extends FlueBaseEvent { @@ -91,10 +78,9 @@ export interface FlueOperationEvent extends FlueBaseEvent { result?: unknown; agentInput?: { text?: string; - images?: Array<{ mimeType?: string }>; }; agentOutput?: - | { type?: "text"; text?: string; finishReason?: string } + | { type?: "text"; text?: string } | { type?: "data"; data?: unknown }; usage?: FlueUsage; } @@ -103,14 +89,6 @@ export interface FlueTurnRequestEvent extends FlueBaseEvent { type: "turn_request"; turnId: string; purpose: FlueTurnPurpose; - model?: string; - provider?: string; - api?: string; - input?: { - systemPrompt?: string; - messages?: unknown[]; - tools?: unknown[]; - }; request?: { api?: string; input?: { @@ -118,14 +96,11 @@ export interface FlueTurnRequestEvent extends FlueBaseEvent { messages?: unknown[]; tools?: unknown[]; }; - model?: string; requestedModel?: string; providerId?: string; providerName?: string; - reasoning?: string; reasoningLevel?: string; }; - reasoning?: string; } export interface FlueTurnEvent extends FlueBaseEvent { @@ -133,12 +108,8 @@ export interface FlueTurnEvent extends FlueBaseEvent { turnId: string; purpose?: FlueTurnPurpose; durationMs?: number; - model?: string; - provider?: string; - api?: string; request?: { api?: string; - model?: string; requestedModel?: string; providerId?: string; providerName?: string; @@ -146,17 +117,11 @@ export interface FlueTurnEvent extends FlueBaseEvent { response?: { output?: unknown; usage?: FlueUsage; - stopReason?: string; finishReason?: string; responseModel?: string; error?: unknown; - errorInfo?: { type?: string; message?: string }; }; - output?: unknown; - usage?: FlueUsage; - stopReason?: string; isError?: boolean; - error?: unknown; } export interface FlueToolStartEvent extends FlueBaseEvent { @@ -164,21 +129,15 @@ export interface FlueToolStartEvent extends FlueBaseEvent { toolName?: string; toolCallId: string; args?: unknown; - arguments?: unknown; - input?: unknown; - origin?: "model" | "caller" | "framework" | "adapter"; - description?: string; } export interface FlueToolCallEvent extends FlueBaseEvent { - type: "tool_call" | "tool"; + type: "tool"; toolName?: string; toolCallId: string; isError?: boolean; result?: unknown; effectiveResult?: unknown; - output?: unknown; - error?: unknown; errorInfo?: { type?: string; message?: string }; durationMs?: number; } @@ -197,9 +156,6 @@ export interface FlueTaskEvent extends FlueBaseEvent { agent?: string; isError?: boolean; result?: unknown; - agentOutput?: - | { type?: "text"; text?: string; finishReason?: string } - | { type?: "data"; data?: unknown }; errorInfo?: { type?: string; message?: string }; durationMs?: number; } @@ -222,12 +178,7 @@ export interface FlueCompactionEvent extends FlueBaseEvent { } export interface FlueContext { - readonly id?: string; - readonly runId?: string; -} - -export interface FlueObservableContext extends FlueContext { - subscribeEvent(callback: (event: unknown) => unknown): () => void; + readonly id: string; } export type FlueExecutionOperation = @@ -244,25 +195,12 @@ export type FlueExecutionOperation = | { type: "task"; taskId: string } | { type: "coordinator"; phase: "reconcile" }; -export interface FlueTraceCarrier { - traceparent: string; - tracestate?: string; -} - export interface FlueExecutionContext { eventContext?: FlueContext; runId?: string; - instanceId?: string; - submissionId?: string; - dispatchId?: string; - agentName?: string; - conversationId?: string; - harness?: string; - session?: string; operationId?: string; turnId?: string; taskId?: string; - traceCarrier?: FlueTraceCarrier; } export type FlueExecutionInterceptor = ( From 5aa890d8a5dee9ab840bbcacadc75933c2cd5f24 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 19 Aug 2026 17:43:44 +0200 Subject: [PATCH 02/19] feat(v4)!: Add Node Engine to package json (#2378) ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) matches what we have documented in readme --- .changeset/add-node-engine.md | 5 +++++ js/package.json | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 .changeset/add-node-engine.md diff --git a/.changeset/add-node-engine.md b/.changeset/add-node-engine.md new file mode 100644 index 000000000..10744b77a --- /dev/null +++ b/.changeset/add-node-engine.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +feat(v4)!: Add Node Engine to package json diff --git a/js/package.json b/js/package.json index 2d0ca836e..3841c8b24 100644 --- a/js/package.json +++ b/js/package.json @@ -174,6 +174,9 @@ }, "author": "", "license": "MIT", + "engines": { + "node": "^20.12.0 || >=22.13.0" + }, "devDependencies": { "@anthropic-ai/sdk": "^0.60.0", "@nodelib/fs.walk": "^1.2.8", From ded0424fb1badc899f70cf7450ea7785beceac03 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 19 Aug 2026 18:43:45 +0200 Subject: [PATCH 03/19] ref(v4)!: Remove JS Braintrust CLI (#2377) Ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) `bt` is supposed to be used over the JS CLI --- .changeset/remove-js-braintrust-cli.md | 5 + .github/workflows/checks.yaml | 16 +- README.md | 2 +- integrations/otel-js/src/otel-compat.test.ts | 20 +- js/dev/authorize.ts | 217 --- js/dev/errorHandler.ts | 37 - js/dev/index.ts | 5 - js/dev/server.test.ts | 21 - js/dev/server.ts | 407 ------ js/dev/stream.ts | 14 - js/dev/types.ts | 123 -- js/eslint.config.ts | 49 +- js/package.json | 34 +- js/scripts/test-output.ts | 194 --- .../shared/src/suites/import-verification.ts | 26 +- js/src/cli/auto-instrumentation.test.ts | 306 ----- js/src/cli/debug-logging.test.ts | 71 - js/src/cli/functions/infer-source.ts | 212 --- js/src/cli/functions/load-module.ts | 54 - js/src/cli/functions/upload.test.ts | 119 -- js/src/cli/functions/upload.ts | 516 ------- js/src/cli/index.ts | 1194 ----------------- js/src/cli/integration.test.ts | 285 ---- js/src/cli/jest/nodeModulesPaths.ts | 74 - js/src/cli/progress.ts | 1 - js/src/cli/reporters/eval.ts | 227 ---- js/src/cli/reporters/progress.ts | 51 - js/src/cli/types.ts | 40 - js/src/cli/util/bundle.ts | 91 -- js/src/cli/util/debug-logging.test.ts | 53 - js/src/cli/util/debug-logging.ts | 37 - .../cli/util/external-packages-plugin.test.ts | 319 ----- js/src/cli/util/external-packages-plugin.ts | 43 - js/src/cli/util/external-packages.test.ts | 199 --- js/src/cli/util/pull.ts | 394 ------ js/src/cli/util/types.ts | 51 - js/src/durable-eval.ts | 1 - js/src/exports.ts | 20 - js/src/framework.test.ts | 30 - js/src/framework.ts | 87 +- js/src/framework2.ts | 96 +- js/src/gitutil.ts | 2 +- js/src/imports.test.ts | 95 +- js/src/node/index.ts | 4 +- js/src/parameters.test.ts | 7 - js/src/reporters/types.ts | 2 +- js/src/zod/zod-serialization-test-shared.ts | 70 - js/src/zod/zod-v3-serialization.test.ts | 234 ---- js/src/zod/zod-v4-serialization.test.ts | 141 -- .../api-compatibility.test.ts | 52 +- js/tsup.config.ts | 27 - knip.jsonc | 2 - package.json | 2 - pnpm-lock.yaml | 333 ++--- turbo.json | 32 - 55 files changed, 192 insertions(+), 6552 deletions(-) create mode 100644 .changeset/remove-js-braintrust-cli.md delete mode 100644 js/dev/authorize.ts delete mode 100644 js/dev/errorHandler.ts delete mode 100644 js/dev/index.ts delete mode 100644 js/dev/server.test.ts delete mode 100644 js/dev/server.ts delete mode 100644 js/dev/stream.ts delete mode 100644 js/dev/types.ts delete mode 100644 js/scripts/test-output.ts delete mode 100644 js/src/cli/auto-instrumentation.test.ts delete mode 100644 js/src/cli/debug-logging.test.ts delete mode 100644 js/src/cli/functions/infer-source.ts delete mode 100644 js/src/cli/functions/load-module.ts delete mode 100644 js/src/cli/functions/upload.test.ts delete mode 100644 js/src/cli/functions/upload.ts delete mode 100755 js/src/cli/index.ts delete mode 100644 js/src/cli/integration.test.ts delete mode 100644 js/src/cli/jest/nodeModulesPaths.ts delete mode 100644 js/src/cli/progress.ts delete mode 100644 js/src/cli/reporters/eval.ts delete mode 100644 js/src/cli/reporters/progress.ts delete mode 100644 js/src/cli/types.ts delete mode 100644 js/src/cli/util/bundle.ts delete mode 100644 js/src/cli/util/debug-logging.test.ts delete mode 100644 js/src/cli/util/debug-logging.ts delete mode 100644 js/src/cli/util/external-packages-plugin.test.ts delete mode 100644 js/src/cli/util/external-packages-plugin.ts delete mode 100644 js/src/cli/util/external-packages.test.ts delete mode 100644 js/src/cli/util/pull.ts delete mode 100644 js/src/cli/util/types.ts delete mode 100644 js/src/zod/zod-serialization-test-shared.ts delete mode 100644 js/src/zod/zod-v3-serialization.test.ts delete mode 100644 js/src/zod/zod-v4-serialization.test.ts diff --git a/.changeset/remove-js-braintrust-cli.md b/.changeset/remove-js-braintrust-cli.md new file mode 100644 index 000000000..9dfca85f2 --- /dev/null +++ b/.changeset/remove-js-braintrust-cli.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref(v4)!: Remove JS Braintrust CLI diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 74b97d78a..4b4d6ce8c 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -236,11 +236,10 @@ jobs: pnpm install --frozen-lockfile || { echo "Baseline install failed"; git checkout "$CURRENT_REF"; exit 0; } pnpm run build || { echo "Baseline build failed"; git checkout "$CURRENT_REF"; exit 0; } - mkdir -p "$BASELINE_DIR/js/dev" "$BASELINE_DIR/js/util" + mkdir -p "$BASELINE_DIR/js/util" cp -R js/dist "$BASELINE_DIR/js/" - cp -R js/dev/dist "$BASELINE_DIR/js/dev/" cp -R js/util/dist "$BASELINE_DIR/js/util/" - echo "Copied js/dist, js/dev/dist, and js/util/dist to baseline" + echo "Copied js/dist and js/util/dist to baseline" git checkout "$CURRENT_REF" @@ -473,17 +472,6 @@ jobs: - name: Run hermetic tests working-directory: ./js run: pnpm run test:checks - - name: Run zod v3 tests - if: matrix.zod-version == '3.25.34' - working-directory: ./js - run: | - pnpm test:zod-v3 - - name: Run zod v4 tests - if: matrix.zod-version == '4.2.1' - working-directory: ./js - run: | - pnpm test:zod-v4 - checks-passed: name: checks-passed needs: diff --git a/README.md b/README.md index d8fd417e4..d9858e7cb 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Eval("Say Hi Bot", { Run it: ```bash -BRAINTRUST_API_KEY= npx braintrust eval tutorial.eval.ts +BRAINTRUST_API_KEY= npx bt eval tutorial.eval.ts ``` ## Packages diff --git a/integrations/otel-js/src/otel-compat.test.ts b/integrations/otel-js/src/otel-compat.test.ts index 6564da531..d06d9f843 100644 --- a/integrations/otel-js/src/otel-compat.test.ts +++ b/integrations/otel-js/src/otel-compat.test.ts @@ -12,7 +12,7 @@ import { getContextManager, BRAINTRUST_CURRENT_SPAN_STORE, _exportsForTestingOnly, - runEvaluator, + Eval, } from "braintrust"; import { BasicTracerProvider, @@ -371,7 +371,7 @@ describe("OTEL compatibility mode", () => { expect(trace2Id).not.toBe(trace3Id); }); - test("OTEL spans in experiment Eval() inherit experiment_id parent", async () => { + test("OTEL spans in Eval() inherit Braintrust parent context", async () => { const { tracer, exporter, processor } = setupOtelFixture( "experiment-eval-test", ); @@ -379,12 +379,11 @@ describe("OTEL compatibility mode", () => { // Capture BT span info from inside the task const btSpanInfo: Array<{ traceId: string; spanId: string }> = []; - // Use runEvaluator with null experiment to avoid API calls - const result = await runEvaluator( - null, + // Use the public Eval API without making Braintrust API calls. + const result = await Eval( + "test-eval-project", { - projectName: "test-eval-project", - evalName: "otel-eval-test", + experimentName: "otel-eval-test", data: [{ input: 1 }, { input: 2 }], task: async (input: number) => { // Capture the current BT span info @@ -408,9 +407,10 @@ describe("OTEL compatibility mode", () => { }, scores: [], }, - new NoopProgressReporter(), - [], - undefined, + { + noSendLogs: true, + progress: new NoopProgressReporter(), + }, ); // Verify we captured BT span info diff --git a/js/dev/authorize.ts b/js/dev/authorize.ts deleted file mode 100644 index 4f7592d17..000000000 --- a/js/dev/authorize.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { IncomingHttpHeaders } from "http"; -import createError from "http-errors"; -import { LRUCache } from "../src/lru-cache"; -import { BraintrustState, LoginOptions, loginToState } from "../src/logger"; - -export interface RequestContext { - appOrigin: string; - token: string | undefined; - state: BraintrustState | undefined; - projectId: string | undefined; -} -declare module "express" { - interface Request { - ctx?: RequestContext; - } -} - -export function authorizeRequest( - req: Request, - res: Response, - next: NextFunction, -) { - try { - const ctx: RequestContext = { - appOrigin: extractAllowedOrigin(req.headers[ORIGIN_HEADER]), - token: undefined, - state: undefined, - projectId: parseHeader(req.headers, PROJECT_ID_HEADER), - }; - - // Extract token and data from request - if ( - req.headers.authorization || - req.headers[BRAINTRUST_AUTH_TOKEN_HEADER] - ) { - const tokenText = parseBraintrustAuthHeader(req.headers); - if (!tokenText) { - return next(createError(400, "Invalid authorization token format")); - } - ctx.token = tokenText.toLowerCase() === "null" ? undefined : tokenText; - } - - req.ctx = ctx; - - next(); // Proceed to next middleware/controller - } catch (e) { - next(e); - } -} - -const loginCache = new LRUCache({ - max: 32, // TODO: Make this configurable -}); - -async function cachedLogin(options: LoginOptions): Promise { - const key = JSON.stringify(options); - const cached = loginCache.get(key); - if (cached) { - return cached; - } - - const state = await loginToState(options); - loginCache.set(key, state); - return state; -} - -export function makeCheckAuthorized(allowedOrgName: string | undefined) { - return async (req: Request, _res: Response, next: NextFunction) => { - if (!req.ctx?.token) { - return next(createError(401, "Unauthorized")); - } - - try { - const orgName = parseHeader(req.headers, "x-bt-org-name"); - - if (!orgName) { - return next(createError(400, "Missing x-bt-org-name header")); - } - - if (allowedOrgName && allowedOrgName !== orgName) { - const errorMessage = `Org '${orgName}' is not allowed. Only org '${allowedOrgName}' is allowed.`; - return next(createError(403, errorMessage)); - } - - const state = await cachedLogin({ - apiKey: req.ctx?.token, - orgName: orgName, - }); - req.ctx.state = state; - next(); - } catch (e) { - console.error("Authorization error:", e); - return next(createError(401, "Unauthorized")); - } - }; -} - -function parseBraintrustAuthHeader( - headers: IncomingHttpHeaders | Record, -): string | undefined { - const tokenString = parseHeader(headers, BRAINTRUST_AUTH_TOKEN_HEADER); - return tokenString ?? parseAuthHeader(headers) ?? undefined; -} - -function parseHeader( - headers: IncomingHttpHeaders | Record, - headerName: string, -): string | undefined { - const token = headers[headerName]; - let tokenString; - if (typeof token === "string") { - tokenString = token; - } else if (Array.isArray(token) && token.length > 0) { - tokenString = token[0]; - } - - return tokenString; -} - -export type StaticOrigin = - | boolean - | string - | RegExp - | Array; - -export function checkOrigin( - requestOrigin: string | undefined, - callback: (err: Error | null, origin?: StaticOrigin) => void, -) { - if (!requestOrigin) { - return callback(null, true); - } - - // the origins can be glob patterns - for (const origin of WHITELISTED_ORIGINS || []) { - if ( - (origin instanceof RegExp && origin.test(requestOrigin)) || - origin === requestOrigin - ) { - return callback(null, requestOrigin); - } - } - - return callback(null, false); -} - -const BRAINTRUST_AUTH_TOKEN_HEADER = "x-bt-auth-token"; -const ORIGIN_HEADER = "origin"; -const PROJECT_ID_HEADER = "x-bt-project-id"; - -function extractAllowedOrigin(originHeader: string | undefined): string { - let allowedOrigin: string = MAIN_ORIGIN; - checkOrigin(originHeader, (err, origin) => { - if (!err && originHeader && origin) { - allowedOrigin = originHeader; - } - }); - return allowedOrigin; -} - -const MAIN_ORIGIN = "https://www.braintrust.dev"; -const WHITELISTED_ORIGINS = [ - MAIN_ORIGIN, - "https://www.braintrustdata.com", - new RegExp("^https://[^/]+\\.preview\\.braintrust\\.dev$"), -] - .concat( - process.env.WHITELISTED_ORIGIN ? [process.env.WHITELISTED_ORIGIN] : [], - ) - .concat( - process.env.BRAINTRUST_APP_URL ? [process.env.BRAINTRUST_APP_URL] : [], - ); - -function parseAuthHeader( - headers: Record, -) { - const authHeader = headers["authorization"]; - let authValue = null; - if (Array.isArray(authHeader)) { - authValue = authHeader[authHeader.length - 1]; - } else { - authValue = authHeader; - } - - if (!authValue) { - return null; - } - - const parts = authValue.split(" "); - if (parts.length !== 2) { - return null; - } - return parts[1]; -} - -export const baseAllowedHeaders = [ - "Content-Type", - "X-Amz-Date", - "Authorization", - "X-Api-Key", - "X-Amz-Security-Token", - "x-bt-auth-token", - "x-bt-parent", - // These are eval-specific - "x-bt-org-name", - "x-bt-project-id", - "x-bt-stream-fmt", - "x-bt-use-cache", - "x-stainless-os", - "x-stainless-lang", - "x-stainless-package-version", - "x-stainless-runtime", - "x-stainless-runtime-version", - "x-stainless-arch", - "x-bt-use-gateway", -]; diff --git a/js/dev/errorHandler.ts b/js/dev/errorHandler.ts deleted file mode 100644 index 12cd15e24..000000000 --- a/js/dev/errorHandler.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from "zod/v3"; -import { Request, Response, ErrorRequestHandler, NextFunction } from "express"; -import { HttpError } from "http-errors"; - -export const errorHandler: ErrorRequestHandler = ( - err: Error | HttpError, - req: Request, - res: Response, - next: NextFunction, -) => { - if ("status" in err) { - res.status(err.status).json({ - error: { - message: err.message, - status: err.status, - }, - }); - return; - } - if (err instanceof z.ZodError) { - res.status(400).json({ - error: { - message: "Invalid request", - errors: err.errors, - }, - }); - return; - } - - console.error("Internal server error", err); - res.status(500).json({ - error: { - message: "Internal server error", - status: 500, - }, - }); -}; diff --git a/js/dev/index.ts b/js/dev/index.ts deleted file mode 100644 index 7ac9af952..000000000 --- a/js/dev/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { configureNode } from "../src/node/config"; - -configureNode(); - -export { runDevServer } from "./server"; diff --git a/js/dev/server.test.ts b/js/dev/server.test.ts deleted file mode 100644 index 2e1461a7e..000000000 --- a/js/dev/server.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { BraintrustState } from "../src/logger"; -import { _exportsForTestingOnly } from "./server"; -import { evalBodySchema } from "./types"; - -describe("remote eval data", () => { - test("uses a named experiment as the data source", async () => { - const request = evalBodySchema.parse({ - name: "remote-evaluator", - data: { experiment_name: "source-experiment" }, - }); - const state = new BraintrustState({}); - - await expect( - _exportsForTestingOnly.getDataset(state, request.data), - ).resolves.toEqual({ - _type: "BaseExperiment", - name: "source-experiment", - }); - }); -}); diff --git a/js/dev/server.ts b/js/dev/server.ts deleted file mode 100644 index 5db8b467f..000000000 --- a/js/dev/server.ts +++ /dev/null @@ -1,407 +0,0 @@ -import express, { NextFunction, Request, Response } from "express"; -import cors from "cors"; -import { - BaseExperiment, - callEvaluatorData, - Eval, - EvalData, - EvalHooks, - EvalScorer, - EvaluatorDef, - OneOrMoreScores, - classifierName, - scorerName, -} from "../src/framework"; -import { errorHandler } from "./errorHandler"; -import { - authorizeRequest, - baseAllowedHeaders, - makeCheckAuthorized, - checkOrigin, -} from "./authorize"; -import { - type FunctionIdType as FunctionId, - type InvokeFunctionType as InvokeFunctionRequest, - type RunEvalType as RunEvalRequest, - type SSEProgressEventDataType as SSEProgressEventData, -} from "../src/generated_types"; -import { - BaseMetadata, - BraintrustState, - EvalCase, - getSpanParentObject, - initDataset, -} from "../src/logger"; -import { - BT_CURSOR_HEADER, - BT_FOUND_EXISTING_HEADER, - parseParent, -} from "../util/index"; -import { serializeSSEEvent } from "./stream"; -import { - evalBodySchema, - EvaluatorDefinitions, - EvaluatorManifest, - type SerializedParametersContainer, -} from "./types"; -import { EvalParameters, validateParameters } from "../src/eval-parameters"; -import { z } from "zod/v3"; -import { ValidationError } from "ajv"; -import { serializeRemoteEvalParametersContainer } from "../src/framework2"; - -export interface DevServerOpts { - host: string; - port: number; - orgName?: string; -} - -export function runDevServer( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - evaluators: EvaluatorDef[], - opts: DevServerOpts, -) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const allEvaluators: EvaluatorManifest = Object.fromEntries( - evaluators.map((evaluator) => [evaluator.evalName, evaluator]), - ) as EvaluatorManifest; - - globalThis._lazy_load = false; - - const app = express(); - - app.use(express.json({ limit: "1gb" })); - console.log("Starting server"); - app.use((req, res, next) => { - if (req.headers["access-control-request-private-network"]) { - res.setHeader("Access-Control-Allow-Private-Network", "true"); - } - next(); - }); - - const checkAuthorized = makeCheckAuthorized(opts.orgName); - - app.use( - cors({ - origin: checkOrigin, - methods: ["GET", "PATCH", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: baseAllowedHeaders, - credentials: true, - exposedHeaders: [ - BT_CURSOR_HEADER, - BT_FOUND_EXISTING_HEADER, - "x-bt-span-id", - "x-bt-span-export", - ], - maxAge: 86400, - }), - ); - - app.use(authorizeRequest); - - app.get("/", (req, res) => { - res.send("Hello, world!"); - }); - - // List endpoint - returns all available evaluators and their metadata - app.get( - "/list", - checkAuthorized, - asyncHandler(async (req, res) => { - const evalDefs: EvaluatorDefinitions = {}; - - for (const [name, evaluator] of Object.entries(allEvaluators)) { - let parameters: SerializedParametersContainer | undefined; - - if (evaluator.parameters) { - const resolvedParams = await Promise.resolve(evaluator.parameters); - parameters = serializeRemoteEvalParametersContainer(resolvedParams); - } - - evalDefs[name] = { - parameters, - scores: (evaluator.scores ?? []).map((score, idx) => ({ - name: scorerName(score, idx), - })), - classifiers: (evaluator.classifiers ?? []).map((classifier, idx) => ({ - name: classifierName(classifier, idx), - })), - }; - } - - res.json(evalDefs); - }), - ); - - app.post( - "/eval", - checkAuthorized, - asyncHandler(async (req, res) => { - const { - name, - parameters, - parent, - experiment_name, - project_id, - data, - scores, - stream, - } = evalBodySchema.parse(req.body); - - if (!req.ctx?.state) { - res - .status(500) - .json({ error: "Braintrust state not initialized in request" }); - return; - } - const state = req.ctx.state; - - const evaluator = allEvaluators[name]; - if (!evaluator) { - res.status(404).json({ error: `Evaluator '${name}' not found` }); - return; - } - - if (evaluator.parameters) { - try { - await validateParameters(parameters ?? {}, evaluator.parameters); - } catch (e) { - console.error("Error validating parameters", e); - if ( - e instanceof z.ZodError || - e instanceof ValidationError || - e instanceof Error - ) { - res.status(400).json({ - error: e.message, - }); - return; - } - throw e; - } - } - - const resolvedData = await getDataset(state, data); - const evalData = callEvaluatorData(resolvedData); - console.log("Starting eval", evaluator.evalName); - - // Set up SSE headers - if (stream) { - res.setHeader("Content-Type", "text/event-stream"); - res.setHeader("Cache-Control", "no-cache"); - res.setHeader("Connection", "keep-alive"); - } else { - res.setHeader("Content-Type", "application/json"); - } - - const task = async ( - input: unknown, - hooks: EvalHooks, - ) => { - const result = await evaluator.task(input, hooks); - - hooks.reportProgress({ - format: "code", - output_type: "completion", - event: "json_delta", - data: JSON.stringify(result), - }); - return result; - }; - - try { - const summary = await Eval( - "worker-thread", - { - ...evaluator, - data: evalData.data, - scores: (evaluator.scores ?? []).concat( - scores?.map((score) => - makeScorer( - state, - score.name, - score.function_id, - req.ctx?.projectId, - ), - ) ?? [], - ), - task, - state, - experimentName: experiment_name ?? undefined, - projectId: project_id ?? undefined, - }, - { - // Avoid printing the bar to the console. - progress: { - start: () => {}, - stop: () => { - console.log("Finished running experiment"); - }, - increment: () => {}, - }, - stream: (data: SSEProgressEventData) => { - if (stream) { - res.write( - serializeSSEEvent({ - event: "progress", - data: JSON.stringify(data), - }), - ); - } - }, - onStart: (metadata) => { - if (stream) { - res.write( - serializeSSEEvent({ - event: "start", - data: JSON.stringify(metadata), - }), - ); - } - }, - parent: parseParent(parent), - parameters: parameters ?? {}, - }, - ); - - if (stream) { - res.write( - serializeSSEEvent({ - event: "summary", - data: JSON.stringify(summary.summary), - }), - ); - res.write( - serializeSSEEvent({ - event: "done", - data: "", - }), - ); - } else { - res.json(summary.summary); - } - } catch (e) { - console.error("Error running eval", e); - if (stream) { - res.write( - serializeSSEEvent({ - event: "error", - data: JSON.stringify(e), - }), - ); - } else { - res.status(500).json({ error: e }); - } - } finally { - res.end(); - } - }), - ); - - app.use(errorHandler); - - // Start the server - app.listen(opts.port, opts.host, () => { - console.log(`Dev server running at http://${opts.host}:${opts.port}`); - }); -} -const asyncHandler = - (fn: (req: Request, res: Response, next: NextFunction) => Promise) => - (req: Request, res: Response, next: NextFunction) => { - Promise.resolve(fn(req, res, next)).catch(next); - }; - -async function getDataset( - state: BraintrustState, - data: RunEvalRequest["data"], -): Promise> { - if ("project_name" in data) { - return initDataset({ - state, - project: data.project_name, - dataset: data.dataset_name, - version: data.dataset_version ?? undefined, - environment: data.dataset_environment ?? undefined, - _internal_btql: data._internal_btql ?? undefined, - }); - } else if ("dataset_id" in data) { - const datasetInfo = await getDatasetById({ - state, - datasetId: data.dataset_id, - }); - return initDataset({ - state, - projectId: datasetInfo.projectId, - dataset: datasetInfo.dataset, - version: data.dataset_version ?? undefined, - environment: data.dataset_environment ?? undefined, - _internal_btql: data._internal_btql ?? undefined, - }); - } else if ("experiment_name" in data) { - return BaseExperiment({ name: data.experiment_name }); - } else { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return data.data as EvalCase[]; - } -} - -const datasetFetchSchema = z.object({ - project_id: z.string(), - name: z.string(), -}); -async function getDatasetById({ - state, - datasetId, -}: { - state: BraintrustState; - datasetId: string; -}): Promise<{ projectId: string; dataset: string }> { - const dataset = await state.appConn().post_json("api/dataset/get", { - id: datasetId, - }); - const parsed = z.array(datasetFetchSchema).parse(dataset); - if (parsed.length === 0) { - throw new Error(`Dataset '${datasetId}' not found`); - } - return { projectId: parsed[0].project_id, dataset: parsed[0].name }; -} - -function makeScorer( - state: BraintrustState, - name: string, - score: FunctionId, - projectId: string | undefined, -): EvalScorer { - const ret = async (input: EvalCase) => { - const request: InvokeFunctionRequest = { - ...score, - input, - parent: await getSpanParentObject().export(), - stream: false, - mode: "auto", - strict: true, - }; - const headers: Record = { - Accept: "application/json", - }; - if (projectId) { - headers["x-bt-project-id"] = projectId; - } - const result = await state.proxyConn().post(`function/invoke`, request, { - headers, - }); - const data = await result.json(); - // NOTE: Ideally we can parse this value with a zod schema. - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return data as OneOrMoreScores; - }; - - Object.defineProperties(ret, { - name: { value: `Remote eval scorer (${name})` }, - }); - - return ret; -} - -export const _exportsForTestingOnly = { - getDataset, -}; diff --git a/js/dev/stream.ts b/js/dev/stream.ts deleted file mode 100644 index 7bca3612b..000000000 --- a/js/dev/stream.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface RawSSEEvent { - id?: string; - event?: string; - data: string; -} - -export function serializeSSEEvent(event: RawSSEEvent): string { - return ( - Object.entries(event) - .filter(([_key, value]) => value !== undefined) - .map(([key, value]) => `${key}: ${value}`) - .join("\n") + "\n\n" - ); -} diff --git a/js/dev/types.ts b/js/dev/types.ts deleted file mode 100644 index da992efea..000000000 --- a/js/dev/types.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - FunctionId as functionIdSchema, - InvokeParent as invokeParentSchema, - RunEval as runEvalSchema, - PromptData as promptDataSchema, -} from "../src/generated_types"; -import { z } from "zod/v3"; -import { EvaluatorDef } from "../src/framework"; -import { BaseMetadata } from "../src/logger"; - -export const evalBodySchema = z.object({ - name: z.string(), - parameters: z.record(z.string(), z.unknown()).nullish(), - data: runEvalSchema.shape.data, - scores: z - .array( - z.object({ - function_id: functionIdSchema, - name: z.string(), - }), - ) - .nullish(), - experiment_name: z.string().nullish(), - project_id: z.string().nullish(), - parent: invokeParentSchema.optional(), - stream: z.boolean().optional(), -}); - -export type EvaluatorManifest = Record< - string, - EvaluatorDef ->; - -export const staticParametersSchema = z.record( - z.string(), - z.union([ - z.object({ - type: z.literal("prompt"), - default: promptDataSchema.optional(), - description: z.string().optional(), - }), - z.object({ - type: z.literal("model"), - default: z.string().optional(), - description: z.string().optional(), - }), - z.object({ - type: z.literal("data"), - schema: z.record(z.unknown()), - default: z.unknown().optional(), - description: z.string().optional(), - }), - ]), -); - -export type StaticParametersSchema = z.infer; - -const evalParametersSerializedSchema = staticParametersSchema; -export type EvalParameterSerializedSchema = z.infer< - typeof evalParametersSerializedSchema ->; - -export const parametersSchema = z.object({ - type: z.literal("object"), - properties: z.record(z.string(), z.record(z.unknown())), - required: z.array(z.string()).optional(), - additionalProperties: z.boolean().optional(), -}); - -export type ParametersSchema = z.infer; - -const parametersSourceSchema = z.object({ - parametersId: z.string().optional(), - slug: z.string(), - name: z.string(), - projectId: z.string().optional(), - version: z.string().optional(), -}); - -export type ParametersSource = z.infer; - -const parametersContainerSchema = z.object({ - type: z.literal("braintrust.parameters"), - schema: parametersSchema, - source: parametersSourceSchema, -}); - -export type ParametersContainer = z.infer; - -const staticParametersContainerSchema = z.object({ - type: z.literal("braintrust.staticParameters"), - schema: staticParametersSchema, - source: z.null().nullish(), -}); - -export type StaticParametersContainer = z.infer< - typeof staticParametersContainerSchema ->; - -export const serializedParametersContainerSchema = z.union([ - parametersContainerSchema, - staticParametersContainerSchema, - // keeping this type here since old versions of the SDK will still pass the unwrapped schema and we need to handle this in the app - staticParametersSchema, -]); - -export type SerializedParametersContainer = z.infer< - typeof serializedParametersContainerSchema ->; - -export const evaluatorDefinitionSchema = z.object({ - parameters: serializedParametersContainerSchema.nullish(), - scores: z.array(z.object({ name: z.string() })).optional(), - classifiers: z.array(z.object({ name: z.string() })).optional(), -}); -export type EvaluatorDefinition = z.infer; - -export const evaluatorDefinitionsSchema = z.record( - z.string(), - evaluatorDefinitionSchema, -); - -export type EvaluatorDefinitions = z.infer; diff --git a/js/eslint.config.ts b/js/eslint.config.ts index 04e2b8f78..80829409c 100644 --- a/js/eslint.config.ts +++ b/js/eslint.config.ts @@ -12,21 +12,18 @@ const tsupConfig = Array.isArray(tsupConfigImport) ? tsupConfigImport : tsupConfigImport.default || []; -// Extract entry points from tsup config, excluding CLI -const entryFiles = tsupConfig - .flatMap((config) => { - const entries = Array.isArray(config.entry) - ? config.entry - : Object.values(config.entry); - return entries; - }) - .filter((entry) => !entry.includes("cli")); +// Extract entry points from tsup config +const entryFiles = tsupConfig.flatMap((config) => { + const entries = Array.isArray(config.entry) + ? config.entry + : Object.values(config.entry); + return entries; +}); export default [ { ignores: [ "dist/**", - "dev/dist/**", "util/dist/**", "node_modules/**", "vendor/**", @@ -164,38 +161,6 @@ export default [ "no-restricted-properties": "off", }, }, - { - files: ["src/**/*.ts", "src/**/*.tsx"], - ignores: [ - "src/cli/**", - "src/debug-logger.ts", - "src/framework.ts", - "src/framework2.ts", - "src/isomorph.ts", - "src/sandbox.ts", - "src/template/**", - "src/reporters/**", - "src/prompt-cache/**", - "src/eval-parameters.ts", - "src/wrappers/**", - "src/instrumentation/**", - "src/auto-instrumentations/**", - ], - rules: { - "no-restricted-imports": [ - "error", - { - patterns: [ - { - group: ["**/cli", "**/cli/**", "./cli", "./cli/**"], - message: - "Importing from 'cli' directory is not allowed. CLI code should not be imported by SDK code.", - }, - ], - }, - ], - }, - }, { files: ["src/**/*.ts", "src/**/*.tsx"], ignores: [...entryFiles, "**/*.test.ts", "**/*.test.tsx"], diff --git a/js/package.json b/js/package.json index 3841c8b24..2f9375376 100644 --- a/js/package.json +++ b/js/package.json @@ -19,7 +19,6 @@ "./dist/index.d.mts": "./dist/browser.d.mts" }, "bin": { - "braintrust": "./dist/cli.js", "bt": "./bin/bt" }, "exports": { @@ -77,12 +76,6 @@ "module": "./dist/index.mjs", "require": "./dist/index.js" }, - "./dev": { - "types": "./dev/dist/index.d.ts", - "import": "./dev/dist/index.mjs", - "module": "./dev/dist/index.mjs", - "require": "./dev/dist/index.js" - }, "./util": { "types": "./util/dist/index.d.ts", "import": "./util/dist/index.mjs", @@ -133,7 +126,6 @@ }, "files": [ "dist/**/*", - "dev/dist/**/*", "util/dist/**/*", "bin/bt", "scripts/bt-helper.js", @@ -146,9 +138,9 @@ "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsup", "check:typings": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", "watch": "tsup --watch", - "clean": "rm -r dist/* && rm -r dev/dist/* && rm -r util/dist/*", + "clean": "rm -r dist/* && rm -r util/dist/*", "docs": "typedoc --options typedoc.json src/node/index.ts", - "test": "vitest run --exclude \"src/wrappers/**/*.test.ts\" --exclude \"src/otel/**/*.test.ts\" --exclude \"smoke/**/*.test.ts\" --exclude \"src/zod/**/*.test.ts\" --exclude \"tests/api-compatibility/**\"", + "test": "vitest run --exclude \"src/wrappers/**/*.test.ts\" --exclude \"src/otel/**/*.test.ts\" --exclude \"smoke/**/*.test.ts\" --exclude \"tests/api-compatibility/**\"", "test:core": "pnpm prune && pnpm test", "test:checks": "pnpm run test:core && pnpm run test:vitest", "test:all": "pnpm run test:checks", @@ -158,18 +150,13 @@ "test:ai-sdk-v1": "vitest run src/wrappers/ai-sdk-v1.test.ts", "test:ai-sdk-v2": "vitest run src/wrappers/ai-sdk-v2.test.ts src/wrappers/ai-sdk-v1.test.ts", "test:ai-sdk-v3": "vitest run src/wrappers/ai-sdk-v3.test.ts", - "test:zod-v3": "vitest run src/zod/zod-v3-serialization.test.ts", - "test:zod-v4": "vitest run src/zod/zod-v4-serialization.test.ts", "test:mastra": "vitest run src/wrappers/mastra.test.ts", "test:vitest": "pnpm --filter @braintrust/vitest-wrapper-tests test", - "test:output": "tsx scripts/test-output.ts --with-comparison --with-metrics --with-progress", "bench": "tsx src/queue.bench.ts", "lint": "eslint .", "fix:lint": "eslint --fix .", "playground": "tsx playground.ts", "playground:auto": "mkdir -p .context && pnpm exec esbuild playground.ts --platform=node --format=esm --outfile=.context/playground.auto.mjs && node --import ./dist/auto-instrumentations/hook.mjs ./.context/playground.auto.mjs", - "playground:cli:push": "node dist/cli.js push playground.ts", - "playground:cli:eval": "node dist/cli.js eval playground.ts", "yalc:publish": "yalc publish" }, "author": "", @@ -179,17 +166,10 @@ }, "devDependencies": { "@anthropic-ai/sdk": "^0.60.0", - "@nodelib/fs.walk": "^1.2.8", - "@types/argparse": "^2.0.14", "@types/async": "^3.2.24", - "@types/cli-progress": "^3.11.5", - "@types/cors": "^2.8.17", "@types/esquery": "^1.5.4", - "@types/express": "^5.0.0", - "@types/http-errors": "^2.0.4", "@types/mustache": "^4.2.5", "@types/node": "^20.10.5", - "@types/pluralize": "^0.0.30", "@typescript-eslint/eslint-plugin": "^8.49.0", "@typescript-eslint/parser": "^8.49.0", "ai": "^6.0.0", @@ -216,31 +196,21 @@ "zod": "^3.25.34" }, "dependencies": { - "@next/env": "^14.2.3", "@vercel/functions": "^1.0.2", "acorn": "^8.16.0", "acorn-import-attributes": "^1.9.5", "ajv": "^8.20.0", - "argparse": "^2.0.1", "astring": "^1.9.0", "cjs-module-lexer": "^2.2.0", - "cli-progress": "^3.12.0", - "cli-table3": "^0.6.5", - "cors": "^2.8.5", "dotenv": "^16.4.5", "esbuild": "0.28.1", "esquery": "^1.7.0", "eventsource-parser": "^1.1.2", - "express": "^5.2.1", - "http-errors": "^2.0.0", "meriyah": "^6.1.4", - "minimatch": "^10.2.5", "module-details-from-path": "^1.0.4", "mustache": "^4.2.0", - "pluralize": "^8.0.0", "semifies": "^1.0.0", "source-map": "^0.7.4", - "termi-link": "^1.0.1", "unplugin": "^2.3.5", "uuid": "^11.1.1", "zod-to-json-schema": "^3.25.0" diff --git a/js/scripts/test-output.ts b/js/scripts/test-output.ts deleted file mode 100644 index 8a9ca0858..000000000 --- a/js/scripts/test-output.ts +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env tsx - -/** - * Test script to preview CLI output formatting without running actual experiments. - * - * Usage: - * pnpm tsx scripts/test-output.ts - * pnpm tsx scripts/test-output.ts --with-comparison - * pnpm tsx scripts/test-output.ts --with-metrics - * pnpm tsx scripts/test-output.ts --with-progress - */ - -import { formatExperimentSummary } from "../src/framework"; -import type { ExperimentSummary } from "../src/logger"; -import { BarProgressReporter } from "../src/cli/progress"; - -function createSampleSummary(options: { - withComparison?: boolean; - withMetrics?: boolean; -}): ExperimentSummary { - const summary: ExperimentSummary = { - projectName: "my-project", - experimentName: "test-experiment", - experimentId: "exp-123", - projectId: "proj-456", - experimentUrl: - "https://www.braintrust.dev/app/my-org/p/my-project/experiments/test-experiment", - projectUrl: "https://www.braintrust.dev/app/my-org/p/my-project", - scores: { - accuracy: { - name: "Accuracy", - score: 0.95, - diff: options.withComparison ? 0.02 : undefined, - improvements: options.withComparison ? 15 : 0, - regressions: options.withComparison ? 3 : 0, - }, - factuality: { - name: "Factuality", - score: 0.87, - diff: options.withComparison ? -0.01 : undefined, - improvements: options.withComparison ? 8 : 0, - regressions: options.withComparison ? 12 : 0, - }, - }, - }; - - if (options.withMetrics) { - summary.metrics = { - duration: { - name: "Duration", - metric: 1.23, - unit: "s", - diff: options.withComparison ? -0.15 : undefined, - improvements: options.withComparison ? 10 : 0, - regressions: options.withComparison ? 5 : 0, - }, - llm_duration: { - name: "LLM duration", - metric: 0.45, - unit: "s", - diff: options.withComparison ? -0.08 : undefined, - improvements: options.withComparison ? 12 : 0, - regressions: options.withComparison ? 3 : 0, - }, - prompt_tokens: { - name: "Prompt tokens", - metric: 4282, - unit: "", - diff: options.withComparison ? 0.035 : undefined, - improvements: options.withComparison ? 0 : 0, - regressions: options.withComparison ? 0 : 0, - }, - completion_tokens: { - name: "Completion tokens", - metric: 310, - unit: "", - diff: options.withComparison ? -0.08 : undefined, - improvements: options.withComparison ? 0 : 0, - regressions: options.withComparison ? 0 : 0, - }, - total_tokens: { - name: "Total tokens", - metric: 4592, - unit: "", - diff: options.withComparison ? 0.027 : undefined, - improvements: options.withComparison ? 0 : 0, - regressions: options.withComparison ? 0 : 0, - }, - estimated_cost: { - name: "Estimated cost", - metric: 0.01, - unit: "$", - diff: options.withComparison ? 0.2 : undefined, - improvements: options.withComparison ? 0 : 0, - regressions: options.withComparison ? 0 : 0, - }, - }; - } - - if (options.withComparison) { - summary.comparisonExperimentName = "baseline-experiment"; - } - - return summary; -} - -async function simulateProgress(evaluatorName: string, total: number) { - const progressReporter = new BarProgressReporter(); - progressReporter.start(evaluatorName, total); - - // Simulate progress with random delays - for (let i = 0; i < total; i++) { - await new Promise((resolve) => - setTimeout(resolve, 50 + Math.random() * 100), - ); - progressReporter.increment(evaluatorName); - } - - progressReporter.stop(); -} - -async function main() { - const args = process.argv.slice(2); - const withComparison = args.includes("--with-comparison"); - const withMetrics = args.includes("--with-metrics"); - const withProgress = args.includes("--with-progress"); - - console.log("\n" + "=".repeat(60)); - console.log("Testing CLI Output Formatting"); - console.log("=".repeat(60) + "\n"); - - // Test 1: Basic summary - console.log("1. Basic Summary (scores only):"); - console.log("-".repeat(60)); - - if (withProgress) { - await simulateProgress("test-experiment", 10); - } - - const basicSummary = createSampleSummary({}); - console.log(formatExperimentSummary(basicSummary)); - console.log("\n"); - - // Test 2: With comparison - if (withComparison) { - console.log("2. Summary with Comparison:"); - console.log("-".repeat(60)); - - if (withProgress) { - await simulateProgress("test-experiment", 15); - } - - const comparisonSummary = createSampleSummary({ withComparison: true }); - console.log(formatExperimentSummary(comparisonSummary)); - console.log("\n"); - } - - // Test 3: With metrics - if (withMetrics) { - console.log("3. Summary with Metrics:"); - console.log("-".repeat(60)); - - if (withProgress) { - await simulateProgress("test-experiment", 20); - } - - const metricsSummary = createSampleSummary({ withMetrics: true }); - console.log(formatExperimentSummary(metricsSummary)); - console.log("\n"); - } - - // Test 4: Full summary - console.log("4. Full Summary (scores + metrics + comparison):"); - console.log("-".repeat(60)); - - if (withProgress) { - await simulateProgress("test-experiment", 25); - } - - const fullSummary = createSampleSummary({ - withComparison: true, - withMetrics: true, - }); - console.log(formatExperimentSummary(fullSummary)); - console.log("\n"); - - console.log("=".repeat(60)); - console.log( - "Done! Use --with-comparison, --with-metrics, and --with-progress flags to see more variations.", - ); - console.log("=".repeat(60) + "\n"); -} - -main(); diff --git a/js/smoke/shared/src/suites/import-verification.ts b/js/smoke/shared/src/suites/import-verification.ts index 492ba2f0e..cf422006b 100644 --- a/js/smoke/shared/src/suites/import-verification.ts +++ b/js/smoke/shared/src/suites/import-verification.ts @@ -54,9 +54,6 @@ export interface BraintrustModule { Eval?: unknown; EvalResultWithSummary?: unknown; Reporter?: unknown; - runEvaluator?: unknown; - buildLocalSummary?: unknown; - reportFailures?: unknown; defaultErrorScoreHandler?: unknown; // Tracing (REQUIRED) @@ -221,27 +218,6 @@ export const testEvalExports = register("testEvalExports", async (module) => { assertDefined(module.Reporter, "Reporter must exist"); assertType(module.Reporter, "function", "Reporter must be a function"); - assertDefined(module.runEvaluator, "runEvaluator must exist"); - assertType( - module.runEvaluator, - "function", - "runEvaluator must be a function", - ); - - assertDefined(module.buildLocalSummary, "buildLocalSummary must exist"); - assertType( - module.buildLocalSummary, - "function", - "buildLocalSummary must be a function", - ); - - assertDefined(module.reportFailures, "reportFailures must exist"); - assertType( - module.reportFailures, - "function", - "reportFailures must be a function", - ); - assertDefined( module.defaultErrorScoreHandler, "defaultErrorScoreHandler must exist", @@ -252,7 +228,7 @@ export const testEvalExports = register("testEvalExports", async (module) => { "defaultErrorScoreHandler must be a function", ); - return "Eval exports verified (7 runtime exports)"; + return "Eval exports verified (4 runtime exports)"; }); export const testTracingExports = register( diff --git a/js/src/cli/auto-instrumentation.test.ts b/js/src/cli/auto-instrumentation.test.ts deleted file mode 100644 index 0b9f41495..000000000 --- a/js/src/cli/auto-instrumentation.test.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import * as fs from "node:fs/promises"; -import * as os from "node:os"; -import * as path from "node:path"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import { fileURLToPath } from "node:url"; -import { newGlobalTracingChannel } from "../global-instrumentation-hooks"; -import { initializeHandles } from "./index"; -import type { FileHandle } from "./types"; - -vi.mock("./functions/load-module", () => ({ - loadModule: () => ({ - evaluators: {}, - functions: [], - parameters: [], - prompts: [], - reporters: {}, - }), -})); - -const googleGenAIChannel = "orchestrion:@google/genai:models.generateContent"; -const anthropicChannel = "orchestrion:@anthropic-ai/sdk:messages.create"; -const execFileAsync = promisify(execFile); -const loadModulePath = fileURLToPath( - new URL("./functions/load-module.ts", import.meta.url), -); -const globalHooksPath = fileURLToPath( - new URL("../global-instrumentation-hooks.ts", import.meta.url), -); -const debugLoggerPath = fileURLToPath( - new URL("../debug-logger.ts", import.meta.url), -); - -describe("eval auto-instrumentation", () => { - let fixtureDir: string; - let handles: Record = {}; - - beforeEach(async () => { - fixtureDir = await fs.mkdtemp( - path.join(os.tmpdir(), "braintrust-eval-instrumentation-"), - ); - await writeFixturePackages(fixtureDir); - }); - - afterEach(async () => { - vi.unstubAllEnvs(); - await Promise.all(Object.values(handles).map((handle) => handle.destroy())); - await Promise.all( - [ - ...new Set( - Object.values(handles) - .map((handle) => handle.bundleFile) - .filter( - (bundleFile): bundleFile is string => bundleFile !== undefined, - ) - .map((bundleFile) => path.dirname(bundleFile)), - ), - ].map((directory) => fs.rm(directory, { recursive: true, force: true })), - ); - handles = {}; - await fs.rm(fixtureDir, { recursive: true, force: true }); - }); - - it.each([ - [ - "directly", - `export { Models } from "@google/genai";`, - (exports: Record) => { - const Models = exports.Models as new () => { - generateContentInternal(input: string): Promise<{ text: string }>; - }; - return new Models().generateContentInternal("payload"); - }, - ], - [ - "through a dependency", - `export { invoke } from "google-genai-client";`, - (exports: Record) => - (exports.invoke as (input: string) => Promise<{ text: string }>)( - "payload", - ), - ], - ])( - "instruments @google/genai when imported %s", - async (_label, source, invoke) => { - const output = await buildEval(fixtureDir, source); - - expect(output).toContain(googleGenAIChannel); - - const lifecycle: string[] = []; - const hook = newGlobalTracingChannel(googleGenAIChannel); - const handlers = { - asyncEnd: () => lifecycle.push("asyncEnd"), - asyncStart: () => lifecycle.push("asyncStart"), - end: () => lifecycle.push("end"), - start: () => lifecycle.push("start"), - }; - hook.subscribe(handlers); - - try { - const loadedModule = { exports: {} as Record }; - Function( - "module", - "exports", - output, - )(loadedModule, loadedModule.exports); - - await expect(invoke(loadedModule.exports)).resolves.toEqual({ - text: "payload", - }); - expect(lifecycle).toEqual(["start", "end", "asyncStart", "asyncEnd"]); - } finally { - hook.unsubscribe(handlers); - } - }, - ); - - it("respects BRAINTRUST_DISABLE_INSTRUMENTATION", async () => { - vi.stubEnv("BRAINTRUST_DISABLE_INSTRUMENTATION", "google-genai"); - - const output = await buildEval( - fixtureDir, - `export { Models } from "@google/genai";`, - ); - - expect(output).not.toContain(googleGenAIChannel); - }); - - it("instruments the final uploaded bundle", async () => { - await buildEval(fixtureDir, `export { Models } from "@google/genai";`); - - await handles[path.join(fixtureDir, "instrumentation.eval.ts")].bundle(); - const output = await fs.readFile( - handles[path.join(fixtureDir, "instrumentation.eval.ts")].bundleFile!, - "utf8", - ); - - expect(output).toContain(googleGenAIChannel); - }); - - it.each([ - ["instruments", undefined, ["start", "end", "asyncStart", "asyncEnd"]], - ["respects opt-out for", "anthropic", []], - ])("%s external eval dependencies", async (_label, disabled, expected) => { - const runnerPath = path.join(fixtureDir, "external-eval-runner.cjs"); - const evalFile = path.join(fixtureDir, "external.eval.ts"); - await fs.writeFile(evalFile, ""); - await fs.writeFile( - runnerPath, - `require("tsx/cjs"); -require("node:module").register = () => {}; -const { loadModule } = require(${JSON.stringify(loadModulePath)}); -const { newGlobalTracingChannel } = require(${JSON.stringify(globalHooksPath)}); -const lifecycle = []; -const channel = newGlobalTracingChannel(${JSON.stringify(anthropicChannel)}); -const handlers = Object.fromEntries( - ["start", "end", "asyncStart", "asyncEnd"].map((name) => [name, () => lifecycle.push(name)]), -); -channel.subscribe(handlers); -loadModule({ - inFile: ${JSON.stringify(evalFile)}, - moduleText: 'const { Messages } = require("@anthropic-ai/sdk/resources/messages/messages.js"); globalThis.__externalEvalResult = new Messages().create("payload");', -}); -Promise.resolve(globalThis.__externalEvalResult).then((result) => { - channel.unsubscribe(handlers); - process.stdout.write(JSON.stringify({ lifecycle, result })); -});`, - ); - - const env = { ...process.env }; - if (disabled) { - env.BRAINTRUST_DISABLE_INSTRUMENTATION = disabled; - } else { - delete env.BRAINTRUST_DISABLE_INSTRUMENTATION; - } - const { stdout } = await execFileAsync(process.execPath, [runnerPath], { - env, - }); - - expect(JSON.parse(stdout)).toEqual({ - lifecycle: expected, - result: { text: "payload" }, - }); - }); - - it("reports unexpected runtime instrumentation failures", async () => { - const runnerPath = path.join( - fixtureDir, - "failed-instrumentation-runner.cjs", - ); - const evalFile = path.join(fixtureDir, "failed-instrumentation.eval.ts"); - await fs.writeFile(evalFile, ""); - await fs.writeFile( - runnerPath, - `require("tsx/cjs"); -require("node:module").register = () => { throw new Error("setup failed"); }; -require(${JSON.stringify(debugLoggerPath)}).setGlobalDebugLogLevel("warn"); -const { loadModule } = require(${JSON.stringify(loadModulePath)}); -loadModule({ inFile: ${JSON.stringify(evalFile)}, moduleText: "" }); -process.stdout.write("loaded");`, - ); - - const { stderr, stdout } = await execFileAsync( - process.execPath, - [runnerPath], - { - env: { ...process.env, BRAINTRUST_DEBUG_LOG_LEVEL: "warn" }, - }, - ); - - expect(stdout).toBe("loaded"); - expect(stderr).toContain( - "Failed to enable auto-instrumentation for external eval dependencies", - ); - expect(stderr).toContain("setup failed"); - }); - - async function buildEval(fixtureDir: string, source: string) { - const evalFile = path.join(fixtureDir, "instrumentation.eval.ts"); - await fs.writeFile(evalFile, source); - handles = await initializeHandles({ files: [evalFile], mode: "eval" }); - - const result = await handles[evalFile].rebuild(); - if (result.type !== "success") { - throw result.error; - } - - return result.result.outputFiles?.[0].text ?? ""; - } -}); - -async function writeFixturePackages(fixtureDir: string) { - const googlePackageDir = path.join(fixtureDir, "node_modules/@google/genai"); - const indirectPackageDir = path.join( - fixtureDir, - "node_modules/google-genai-client", - ); - const anthropicPackageDir = path.join( - fixtureDir, - "node_modules/@anthropic-ai/sdk", - ); - await fs.mkdir(path.join(googlePackageDir, "dist/node"), { - recursive: true, - }); - await fs.mkdir(indirectPackageDir, { recursive: true }); - await fs.mkdir(path.join(anthropicPackageDir, "resources/messages"), { - recursive: true, - }); - - await Promise.all([ - fs.writeFile( - path.join(googlePackageDir, "package.json"), - JSON.stringify({ - name: "@google/genai", - version: "1.50.0", - type: "module", - exports: "./dist/node/index.mjs", - }), - ), - fs.writeFile( - path.join(googlePackageDir, "dist/node/index.mjs"), - `export class Models { - async generateContentInternal(input) { - return { text: input }; - } - }`, - ), - fs.writeFile( - path.join(indirectPackageDir, "package.json"), - JSON.stringify({ - name: "google-genai-client", - version: "1.0.0", - type: "module", - exports: "./index.mjs", - }), - ), - fs.writeFile( - path.join(indirectPackageDir, "index.mjs"), - `import { Models } from "@google/genai"; - export function invoke(input) { - return new Models().generateContentInternal(input); - }`, - ), - fs.writeFile( - path.join(anthropicPackageDir, "package.json"), - JSON.stringify({ - name: "@anthropic-ai/sdk", - version: "0.60.0", - exports: { - "./resources/messages/messages.js": - "./resources/messages/messages.js", - }, - }), - ), - fs.writeFile( - path.join(anthropicPackageDir, "resources/messages/messages.js"), - `class Messages { - async create(input) { - return { text: input }; - } - } - module.exports = { Messages };`, - ), - ]); -} diff --git a/js/src/cli/debug-logging.test.ts b/js/src/cli/debug-logging.test.ts deleted file mode 100644 index 49e352679..000000000 --- a/js/src/cli/debug-logging.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; - -vi.mock("@next/env", () => ({ - loadEnvConfig: vi.fn(), -})); - -vi.mock("dotenv", () => ({ - config: vi.fn(() => ({})), -})); - -vi.mock("./index", () => ({ - handleBuildFailure: vi.fn(), - initializeHandles: vi.fn(), -})); - -vi.mock("../logger", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - login: vi.fn().mockResolvedValue(undefined), - }; -}); - -import { login } from "../logger"; -import { resetDebugLoggingArgsForTests } from "./util/debug-logging"; -import { loadCLIEnv } from "./util/bundle"; - -describe("CLI debug logging", () => { - beforeEach(() => { - vi.clearAllMocks(); - resetDebugLoggingArgsForTests(); - }); - - test("loadCLIEnv forwards debugLogLevel to login", async () => { - await loadCLIEnv({ - verbose: false, - api_key: "test-key", - org_name: "test-org", - app_url: "https://braintrust.dev", - debug_logging: "debug", - }); - - expect(login).toHaveBeenCalledWith({ - apiKey: "test-key", - orgName: "test-org", - appUrl: "https://braintrust.dev", - debugLogLevel: "debug", - }); - }); - - test("loadCLIEnv treats --verbose as a deprecated alias for --debug-logging debug", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - - await loadCLIEnv({ - verbose: true, - api_key: "test-key", - org_name: "test-org", - app_url: "https://braintrust.dev", - }); - - expect(login).toHaveBeenCalledWith({ - apiKey: "test-key", - orgName: "test-org", - appUrl: "https://braintrust.dev", - debugLogLevel: "debug", - }); - expect(warnSpy).toHaveBeenCalledWith( - "Warning: --verbose is deprecated and will be removed in a future version of braintrust. Use --debug-logging debug to see full stack traces and troubleshooting details.", - ); - }); -}); diff --git a/js/src/cli/functions/infer-source.ts b/js/src/cli/functions/infer-source.ts deleted file mode 100644 index 0d480c621..000000000 --- a/js/src/cli/functions/infer-source.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { SourceMapConsumer } from "source-map"; -import * as fs from "node:fs/promises"; -import { EvaluatorFile, warning } from "../../framework"; -import { loadModule } from "./load-module"; -import { type CodeBundleType as CodeBundle } from "../../generated_types"; -import path from "node:path"; -import type { Node } from "typescript"; - -interface SourceMapContext { - inFiles: Record; - outFileModule: EvaluatorFile; - outFileLines: string[]; - sourceMapDir: string; - sourceMap: SourceMapConsumer; -} - -export async function makeSourceMapContext({ - inFile, - outFile, - sourceMapFile, -}: { - inFile: string; - outFile: string; - sourceMapFile: string; -}): Promise { - const [inFileContents, outFileContents, sourceMap] = await Promise.all([ - fs.readFile(inFile, "utf8"), - fs.readFile(outFile, "utf8"), - (async () => { - const sourceMap = await fs.readFile(sourceMapFile, "utf8"); - const sourceMapJSON = JSON.parse(sourceMap); - return new SourceMapConsumer(sourceMapJSON); - })(), - ]); - return { - inFiles: { [inFile]: inFileContents.split("\n") }, - outFileModule: loadModule({ inFile, moduleText: outFileContents }), - outFileLines: outFileContents.split("\n"), - sourceMapDir: path.dirname(sourceMapFile), - sourceMap, - }; -} - -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -function isNative(fn: Function): boolean { - return /\{\s*\[native code\]\s*\}/.test(Function.prototype.toString.call(fn)); -} - -function locationToString(location: CodeBundle["location"]): string { - if (location.type === "experiment") { - return `eval ${location.eval_name} -> ${location.position.type}`; - } else if (location.type === "function") { - return `task ${location.index}`; - } else if (location.type === "sandbox") { - return `sandbox eval ${location.eval_name}`; - } else { - throw new Error(`Unknown location type`); - } -} - -export async function findCodeDefinition({ - location, - ctx: { inFiles, outFileModule, outFileLines, sourceMapDir, sourceMap }, -}: { - location: CodeBundle["location"]; - ctx: SourceMapContext; -}): Promise { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - let fn: Function | undefined = undefined; - - if (location.type === "experiment" || location.type === "sandbox") { - const evaluator = outFileModule.evaluators[location.eval_name]?.evaluator; - if (!evaluator) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Warning: failed to find evaluator for ${location.eval_name}. Will not display preview.`, - ), - ); - return undefined; - } - - if (location.type === "sandbox") { - fn = evaluator.task; - } else { - fn = - location.position.type === "task" - ? evaluator.task - : location.position.type === "scorer" - ? (evaluator.scores ?? [])[location.position.index] - : (evaluator.classifiers ?? [])[location.position.index]; - } - } else if (location.type === "function") { - fn = outFileModule.functions[location.index].handler; - } else { - throw new Error(`Unknown location type`); - } - - if (!fn) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Warning: failed to find ${locationToString(location)}. Will not display preview.`, - ), - ); - return undefined; - } - - const sourceCode = fn.toString(); - if (isNative(fn)) { - return undefined; - } - let lineNumber = 0; - let columnNumber = -1; - for (const line of outFileLines) { - const sourceDefinition = line.indexOf(sourceCode); - if (sourceDefinition !== -1) { - columnNumber = sourceDefinition; - break; - } - lineNumber++; - } - - if (columnNumber === -1) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Warning: failed to find code definition for ${fn.name}. Will not display preview.`, - ), - ); - return undefined; - } - const originalPosition = sourceMap.originalPositionFor({ - line: lineNumber + 1, - column: columnNumber + 1, - }); - - if (originalPosition.source === null || originalPosition.line === null) { - return undefined; - } - - if (!inFiles[originalPosition.source]) { - const originalFile = path.join(sourceMapDir, originalPosition.source); - inFiles[originalPosition.source] = ( - await fs.readFile(originalFile, "utf-8") - ).split("\n"); - } - - const originalLines = inFiles[originalPosition.source]; - - // Parse the file with Typescript to find the function definition - const ts = await getTsModule(); - if (!ts) { - return undefined; - } - const sourceFile = ts.createSourceFile( - originalPosition.source, - originalLines.join("\n"), - ts.ScriptTarget.Latest, - true, - ); - let functionNode: Node | undefined = undefined; - const targetPosition = ts.getPositionOfLineAndCharacter( - sourceFile, - originalPosition.line - 1, - originalPosition.column || 0, - ); - - ts.forEachChild(sourceFile, function visit(node) { - if (node.pos <= targetPosition && targetPosition < node.end) { - if ( - ts.isFunctionDeclaration(node) || - ts.isFunctionExpression(node) || - ts.isArrowFunction(node) - ) { - functionNode = node; - } else { - ts.forEachChild(node, visit); - } - } - }); - - if (!functionNode) { - return undefined; - } - - const printer = ts.createPrinter(); - const functionDefinition = printer.printNode( - ts.EmitHint.Unspecified, - functionNode, - sourceFile, - ); - - return functionDefinition; -} - -let tsModule: typeof import("typescript") | undefined = undefined; -async function getTsModule() { - if (!tsModule) { - try { - tsModule = require("typescript"); - } catch { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - "Failed to load TypeScript module. Will not use TypeScript to derive preview.", - ), - ); - } - } - return tsModule; -} diff --git a/js/src/cli/functions/load-module.ts b/js/src/cli/functions/load-module.ts deleted file mode 100644 index 1fa7296b0..000000000 --- a/js/src/cli/functions/load-module.ts +++ /dev/null @@ -1,54 +0,0 @@ -import nodeModulesPaths from "../jest/nodeModulesPaths"; -import path, { dirname } from "node:path"; -import { _internalGetGlobalState } from "../../logger"; -import { EvaluatorFile } from "../../framework"; -import { applyAutoInstrumentation } from "../../node/apply-auto-instrumentation"; -import { debugLogger } from "../../debug-logger"; - -function evalWithModuleContext(inFile: string, evalFn: () => T): T { - const modulePaths = [...module.paths]; - try { - module.paths = nodeModulesPaths(path.dirname(inFile), {}); - return evalFn(); - } finally { - module.paths = modulePaths; - } -} - -export function loadModule({ - inFile, - moduleText, -}: { - inFile: string; - moduleText: string; -}): EvaluatorFile { - return evalWithModuleContext(inFile, () => { - globalThis._evals = { - functions: [], - prompts: [], - parameters: [], - evaluators: {}, - reporters: {}, - }; - globalThis._lazy_load = true; - const state = _internalGetGlobalState(); - (globalThis as any)[Symbol.for("braintrust-state")] = state; - const __filename = inFile; - const __dirname = dirname(__filename); - try { - applyAutoInstrumentation(); - } catch (error) { - debugLogger.warn( - "Failed to enable auto-instrumentation for external eval dependencies; bundled dependencies remain instrumented:", - error, - ); - } - new Function("require", "module", "__filename", "__dirname", moduleText)( - require, - module, - __filename, - __dirname, - ); - return { ...globalThis._evals }; - }); -} diff --git a/js/src/cli/functions/upload.test.ts b/js/src/cli/functions/upload.test.ts deleted file mode 100644 index f0ecde997..000000000 --- a/js/src/cli/functions/upload.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import { buildBundledFunctionEntry } from "./upload"; - -vi.mock("./infer-source", () => ({ - findCodeDefinition: vi.fn(), - makeSourceMapContext: vi.fn(), -})); - -describe("buildBundledFunctionEntry", () => { - test("preserves tags and existing function fields", async () => { - const spec = { - project_id: "proj-123", - name: "test-tool", - slug: "test-tool", - description: "Test tool", - location: { - type: "function" as const, - index: 0, - }, - function_type: "tool" as const, - origin: { - object_type: "experiment" as const, - object_id: "exp-123", - internal: false, - }, - function_schema: { - parameters: { type: "object" }, - returns: { type: "string" }, - }, - if_exists: "replace" as const, - tags: ["ci", "sdk"], - metadata: { owner: "sdk" }, - }; - - const entry = await buildBundledFunctionEntry({ - spec, - runtime_context: { runtime: "node", version: "22.0.0" }, - bundleId: "bundle-123", - sourceMapContext: undefined, - }); - - expect(entry).toMatchObject({ - project_id: "proj-123", - name: "test-tool", - slug: "test-tool", - description: "Test tool", - origin: spec.origin, - function_type: "tool", - function_schema: spec.function_schema, - if_exists: "replace", - tags: ["ci", "sdk"], - metadata: { owner: "sdk" }, - function_data: { - type: "code", - data: { - type: "bundle", - runtime_context: { runtime: "node", version: "22.0.0" }, - location: spec.location, - bundle_id: "bundle-123", - }, - }, - }); - expect(((entry.function_data as any).data as any).preview).toBeUndefined(); - }); - - test("does not invent tags when they are omitted", async () => { - const entry = await buildBundledFunctionEntry({ - spec: { - project_id: "proj-123", - name: "test-tool", - slug: "test-tool", - description: "Test tool", - location: { - type: "function" as const, - index: 0, - }, - function_type: "tool" as const, - }, - runtime_context: { runtime: "node", version: "22.0.0" }, - bundleId: "bundle-123", - sourceMapContext: undefined, - }); - - expect(entry.tags).toBeUndefined(); - }); - - test("preserves classifier experiment locations", async () => { - const entry = await buildBundledFunctionEntry({ - spec: { - project_id: "proj-123", - name: "test-classifier", - slug: "test-classifier", - description: "Test classifier", - location: { - type: "experiment" as const, - eval_name: "eval-1", - position: { - type: "classifier" as const, - index: 0, - }, - }, - function_type: "classifier" as const, - }, - runtime_context: { runtime: "node", version: "22.0.0" }, - bundleId: "bundle-123", - sourceMapContext: undefined, - }); - - expect(entry.function_type).toBe("classifier"); - expect(((entry.function_data as any).data as any).location).toEqual({ - type: "experiment", - eval_name: "eval-1", - position: { - type: "classifier", - index: 0, - }, - }); - }); -}); diff --git a/js/src/cli/functions/upload.ts b/js/src/cli/functions/upload.ts deleted file mode 100644 index da513f503..000000000 --- a/js/src/cli/functions/upload.ts +++ /dev/null @@ -1,516 +0,0 @@ -import { - type CodeBundleType as CodeBundle, - type FunctionType as FunctionObject, - type IfExistsType as IfExists, -} from "../../generated_types"; -import type { BuildSuccess, FileHandle } from "../types"; -import { classifierName, scorerName, warning } from "../../framework"; -import { - _internalGetGlobalState, - Experiment, - FailedHTTPResponse, -} from "../../logger"; -import * as esbuild from "esbuild"; -import fs from "node:fs"; -import path from "node:path"; -import { createGzip } from "node:zlib"; -import { addAzureBlobHeaders, isEmpty } from "../../util"; -import { z } from "zod/v3"; -import { capitalize } from "../../../util/index"; -import { findCodeDefinition, makeSourceMapContext } from "./infer-source"; -import { slugify } from "../../../util/string_util"; -import { zodToJsonSchema } from "../../zod/utils"; -import pluralize from "pluralize"; -import { - FunctionEvent, - ProjectNameIdMap, - serializeRemoteEvalParametersContainer, -} from "../../framework2"; - -interface BundledFunctionSpec { - project_id: string; - name: string; - slug: string; - description: string; - location: CodeBundle["location"]; - function_type: FunctionObject["function_type"]; - origin?: FunctionObject["origin"]; - function_schema?: FunctionObject["function_schema"]; - if_exists?: IfExists; - tags?: string[]; - metadata?: Record; -} - -type BundledFunctionEntry = FunctionEvent & { - origin?: FunctionObject["origin"]; - function_schema?: FunctionObject["function_schema"]; -}; - -const SANDBOX_GROUP_NAME_METADATA_KEY = "_bt_sandbox_group_name"; - -const pathInfoSchema = z - .strictObject({ - url: z.string(), - bundleId: z.string(), - }) - .strip(); - -export async function uploadHandleBundles({ - buildResults, - evalToExperiment, - bundlePromises, - handles, - setCurrent, - showDetailedErrors, - defaultIfExists, -}: { - buildResults: BuildSuccess[]; - evalToExperiment?: Record>; - bundlePromises: { - [k: string]: Promise>; - }; - handles: Record; - showDetailedErrors: boolean; - setCurrent: boolean; - defaultIfExists: IfExists; -}) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - `Processing ${buildResults.length} ${pluralize("file", buildResults.length)}...`, - ); - - const projectNameToId = new ProjectNameIdMap(); - - const uploadPromises = buildResults.map(async (result) => { - if (result.type !== "success") { - return; - } - const sourceFile = result.sourceFile; - - const bundleSpecs: BundledFunctionSpec[] = []; - const prompts: FunctionEvent[] = []; - - if (setCurrent) { - for (let i = 0; i < result.evaluator.functions.length; i++) { - const fn = result.evaluator.functions[i]; - const project_id = await projectNameToId.resolve(fn.project); - - bundleSpecs.push({ - project_id: project_id, - name: fn.name, - slug: fn.slug, - description: fn.description ?? "", - function_type: fn.type, - location: { - type: "function", - index: i, - }, - function_schema: - fn.parameters || fn.returns - ? { - parameters: fn.parameters - ? zodToJsonSchema(fn.parameters) - : undefined, - returns: fn.returns ? zodToJsonSchema(fn.returns) : undefined, - } - : undefined, - if_exists: fn.ifExists, - tags: fn.tags, - metadata: fn.metadata, - }); - } - - for (const prompt of result.evaluator.prompts) { - prompts.push(await prompt.toFunctionDefinition(projectNameToId)); - } - - if (result.evaluator.parameters != null) { - for (const param of result.evaluator.parameters) { - prompts.push(await param.toFunctionDefinition(projectNameToId)); - } - } - } - - for (const evaluator of Object.values(result.evaluator.evaluators)) { - const experiment = - evalToExperiment?.[sourceFile]?.[evaluator.evaluator.evalName]; - - const baseInfo = { - project_id: experiment - ? (await experiment.project).id - : await projectNameToId.getId(evaluator.evaluator.projectName), - }; - - const namePrefix = setCurrent - ? evaluator.evaluator.experimentName - ? `${evaluator.evaluator.experimentName}` - : evaluator.evaluator.evalName - : experiment - ? `${await experiment.name}` - : evaluator.evaluator.evalName; - - const experimentId = experiment ? await experiment.id : undefined; - const origin: FunctionObject["origin"] = experimentId - ? { - object_type: "experiment", - object_id: experimentId, - internal: !setCurrent, - } - : undefined; - - const fileSpecs: BundledFunctionSpec[] = [ - { - ...baseInfo, - // There is a very small chance that someone names a function with the same convention, but - // let's assume it's low enough that it doesn't matter. - ...formatNameAndSlug(["eval", namePrefix, "task"]), - description: `Task for eval ${namePrefix}`, - location: { - type: "experiment", - eval_name: evaluator.evaluator.evalName, - position: { type: "task" }, - }, - function_type: "task", - origin, - }, - ...(evaluator.evaluator.scores ?? []).map( - (score, i): BundledFunctionSpec => { - const name = scorerName(score, i); - return { - ...baseInfo, - // There is a very small chance that someone names a function with the same convention, but - // let's assume it's low enough that it doesn't matter. - ...formatNameAndSlug(["eval", namePrefix, "scorer", name]), - description: `Score ${name} for eval ${namePrefix}`, - location: { - type: "experiment", - eval_name: evaluator.evaluator.evalName, - position: { type: "scorer", index: i }, - }, - function_type: "scorer", - origin, - }; - }, - ), - ...(evaluator.evaluator.classifiers ?? []).map( - (classifier, i): BundledFunctionSpec => { - const name = classifierName(classifier, i); - return { - ...baseInfo, - ...formatNameAndSlug(["eval", namePrefix, "classifier", name]), - description: `Classifier ${name} for eval ${namePrefix}`, - location: { - type: "experiment", - eval_name: evaluator.evaluator.evalName, - position: { type: "classifier", index: i }, - }, - function_type: "classifier", - origin, - }; - }, - ), - ]; - - bundleSpecs.push(...fileSpecs); - - if (setCurrent) { - const sourceStem = path - .basename(sourceFile, path.extname(sourceFile)) - .replace(/\.eval$/, ""); - const evalName = evaluator.evaluator.evalName; - const sandboxGroupName = sourceStem; - - const resolvedParameters = evaluator.evaluator.parameters - ? await Promise.resolve(evaluator.evaluator.parameters) - : undefined; - - const evaluatorDefinition = { - ...(resolvedParameters - ? { - parameters: - serializeRemoteEvalParametersContainer(resolvedParameters), - } - : {}), - scores: (evaluator.evaluator.scores ?? []).map((score, i) => ({ - name: scorerName(score, i), - })), - classifiers: (evaluator.evaluator.classifiers ?? []).map( - (classifier, i) => ({ - name: classifierName(classifier, i), - }), - ), - }; - - bundleSpecs.push({ - ...baseInfo, - name: `Eval ${evalName} sandbox`, - slug: slugify(`${sourceStem}-${evalName}-sandbox`), - description: `Sandbox eval ${evalName}`, - location: { - type: "sandbox", - sandbox_spec: { - provider: "lambda", - }, - entrypoints: [sourceFile], - eval_name: evalName, - evaluator_definition: evaluatorDefinition, - }, - function_type: "sandbox", - metadata: { - [SANDBOX_GROUP_NAME_METADATA_KEY]: sandboxGroupName, - }, - origin, - }); - } - } - - const slugs: Set = new Set(); - for (const spec of bundleSpecs) { - if (slugs.has(spec.slug)) { - throw new Error(`Duplicate slug: ${spec.slug}`); - } - slugs.add(spec.slug); - } - for (const prompt of prompts) { - if (slugs.has(prompt.slug)) { - throw new Error(`Duplicate slug: ${prompt.slug}`); - } - slugs.add(prompt.slug); - } - - return await uploadBundles({ - sourceFile, - prompts, - bundleSpecs, - bundlePromises, - handles, - defaultIfExists, - showDetailedErrors, - }); - }); - - const uploadResults = await Promise.all(uploadPromises); - const numUploaded = uploadResults.length; - const numFailed = uploadResults.filter((result) => !result).length; - - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - `${numUploaded} ${pluralize("file", numUploaded)} uploaded ${ - numFailed > 0 - ? `with ${numFailed} error${numFailed > 1 ? "s" : ""}` - : "successfully" - }.`, - ); - - return { - numTotal: buildResults.length, - numUploaded, - numFailed, - }; -} - -async function uploadBundles({ - sourceFile, - prompts, - bundleSpecs, - bundlePromises, - handles, - defaultIfExists, - showDetailedErrors, -}: { - sourceFile: string; - prompts: FunctionEvent[]; - bundleSpecs: BundledFunctionSpec[]; - bundlePromises: { - [k: string]: Promise>; - }; - handles: Record; - defaultIfExists: IfExists; - showDetailedErrors: boolean; -}): Promise { - const orgId = _internalGetGlobalState().orgId; - if (!orgId) { - throw new Error("No organization ID found"); - } - - const loggerConn = _internalGetGlobalState().apiConn(); - const runtime_context = { - runtime: "node", - version: process.version.slice(1), - } as const; - - const bundle = await bundlePromises[sourceFile]; - const bundleFileName = handles[sourceFile].bundleFile; - if (!bundle || !bundleFileName) { - return false; - } - - const sourceMapContextPromise = makeSourceMapContext({ - inFile: sourceFile, - outFile: bundleFileName, - sourceMapFile: bundleFileName + ".map", - }); - - let pathInfo: z.infer | undefined = undefined; - if (bundleSpecs.length > 0) { - try { - pathInfo = pathInfoSchema.parse( - await loggerConn.post_json("function/code", { - org_id: orgId, - runtime_context, - }), - ); - } catch (e) { - if (showDetailedErrors) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(e); - } - const msg = - e instanceof FailedHTTPResponse - ? `Unable to upload your code. ${e.status} (${e.text}): ${e.data}` - : `Unable to upload your code. You most likely need to update the API: ${e}`; - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(warning(msg)); - return false; - } - } - - // Upload bundleFile to pathInfo.url - if (isEmpty(bundleFileName)) { - throw new Error("No bundle file found"); - } - const bundleFile = path.resolve(bundleFileName); - const uploadPromise = (async (): Promise => { - if (!pathInfo) { - return true; - } - const bundleStream = fs.createReadStream(bundleFile).pipe(createGzip()); - const bundleData = await new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - bundleStream.on("data", (chunk) => { - chunks.push(chunk); - }); - bundleStream.on("end", () => { - resolve(Buffer.concat(chunks)); - }); - bundleStream.on("error", reject); - }); - - const headers = { - "Content-Encoding": "gzip", - }; - addAzureBlobHeaders(headers, pathInfo.url); - - const resp = await fetch(pathInfo.url, { - method: "PUT", - body: bundleData, - headers, - }); - if (!resp.ok) { - throw new Error( - `Failed to upload bundle: ${resp.status} ${await resp.text()}`, - ); - } - return true; - })(); - - const sourceMapContext = await sourceMapContextPromise; - - // Insert the spec as prompt data - const functionEntries: FunctionEvent[] = [ - ...prompts, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ...((await Promise.all( - bundleSpecs.map((spec) => - buildBundledFunctionEntry({ - spec, - runtime_context, - bundleId: pathInfo!.bundleId, - sourceMapContext, - }), - ), - )) as FunctionEvent[]), - ].map((fn) => ({ - ...fn, - if_exists: fn.if_exists ?? defaultIfExists, - })); - - const logPromise = (async (): Promise => { - try { - await _internalGetGlobalState().apiConn().post_json("insert-functions", { - functions: functionEntries, - }); - } catch (e) { - if (showDetailedErrors) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(e); - } - const msg = - e instanceof FailedHTTPResponse - ? `Failed to save function definitions for '${sourceFile}'. ${e.status} (${e.text}): ${e.data}` - : `Failed to save function definitions for '${sourceFile}'. You most likely need to update the API: ${e}`; - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn(warning(msg)); - return false; - } - return true; - })(); - - const [uploadSuccess, logSuccess] = await Promise.all([ - uploadPromise, - logPromise, - ]); - - return uploadSuccess && logSuccess; -} - -function formatNameAndSlug(pieces: string[]) { - const nonEmptyPieces = pieces.filter((piece) => piece.trim() !== ""); - return { - name: capitalize(nonEmptyPieces.join(" ")), - slug: slugify(nonEmptyPieces.join("-")), - }; -} - -export async function buildBundledFunctionEntry({ - spec, - runtime_context, - bundleId, - sourceMapContext, -}: { - spec: BundledFunctionSpec; - runtime_context: { - runtime: "node"; - version: string; - }; - bundleId: string; - sourceMapContext?: Awaited>; -}): Promise { - return { - project_id: spec.project_id, - name: spec.name, - slug: spec.slug, - description: spec.description, - function_data: { - type: "code", - data: { - type: "bundle", - runtime_context, - location: spec.location, - bundle_id: bundleId, - preview: sourceMapContext - ? await findCodeDefinition({ - location: spec.location, - ctx: sourceMapContext, - }) - : undefined, - }, - }, - origin: spec.origin, - function_type: spec.function_type ?? undefined, - function_schema: spec.function_schema, - if_exists: spec.if_exists, - tags: spec.tags, - metadata: spec.metadata, - }; -} diff --git a/js/src/cli/index.ts b/js/src/cli/index.ts deleted file mode 100755 index eb5004555..000000000 --- a/js/src/cli/index.ts +++ /dev/null @@ -1,1194 +0,0 @@ -#!/usr/bin/env node - -import * as esbuild from "esbuild"; -import * as dotenv from "dotenv"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import util, { styleText } from "node:util"; -import * as fsWalk from "@nodelib/fs.walk"; -import { minimatch } from "minimatch"; -import { ArgumentParser } from "argparse"; -import { v4 as uuidv4 } from "uuid"; -import pluralize from "pluralize"; -import { - login, - init as _initExperiment, - Experiment, - BaseMetadata, - Dataset, - type ParametersRef, - RemoteEvalParameters, -} from "../logger"; -import type { ProgressReporter } from "../reporters/types"; -import { - BarProgressReporter, - SimpleProgressReporter, -} from "./reporters/progress"; -import { terminalLink } from "termi-link"; - -// Re-use the module resolution logic from Jest -import type { ReporterDef } from "../reporters/types"; -import { - EvalData, - EvaluatorDef, - EvaluatorFile, - Filter, - callEvaluatorData, - error, - logError, - parseFilters, - runEvaluator, -} from "../framework"; -import { fancyReporter, warning } from "./reporters/eval"; -import { configureNode } from "../node/config"; -import { isEmpty } from "../util"; -import { loadEnvConfig } from "@next/env"; -import type { - BuildSuccess, - BuildFailure, - BtBuildResult, - FileHandle, - EvaluatorState, -} from "./types"; -import { uploadHandleBundles } from "./functions/upload"; -import { loadModule } from "./functions/load-module"; -import { bundleCommand } from "./util/bundle"; -import { RunArgs } from "./util/types"; -import { - normalizeDebugLoggingArgs, - shouldShowDetailedErrors, -} from "./util/debug-logging"; -import { pullCommand } from "./util/pull"; -import { runDevServer } from "../../dev/server"; -import { braintrustEsbuildPlugin } from "../auto-instrumentations/bundler/esbuild"; - -// This requires require -// https://stackoverflow.com/questions/50822310/how-to-import-package-json-in-typescript -const { version } = require("../../package.json"); - -// TODO: This could be loaded from configuration -const INCLUDE_EVAL = [ - "**/*.eval.ts", - "**/*.eval.tsx", - "**/*.eval.js", - "**/*.eval.jsx", -]; -const INCLUDE_BUNDLE = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]; -const EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/build/**"]; -const OUT_EXT = "js"; - -configureNode(); - -function evaluateBuildResults( - inFile: string, - buildResult: esbuild.BuildResult, -): EvaluatorFile | null { - if (!buildResult.outputFiles) { - return null; - } - const moduleText = buildResult.outputFiles[0].text; - return loadModule({ inFile, moduleText }); -} - -async function initExperiment( - evaluator: EvaluatorDef, - evaluatorData: { - data: EvalData; - baseExperiment: string | undefined; - }, -) { - const { data, baseExperiment: defaultBaseExperiment } = evaluatorData; - const parameters = await getExperimentParametersRef(evaluator.parameters); - // NOTE: This code is duplicated with initExperiment in js/src/framework.ts. - // Make sure to update that if you change this. - const logger = _initExperiment({ - state: evaluator.state, - ...(evaluator.projectId - ? { projectId: evaluator.projectId } - : { project: evaluator.projectName }), - experiment: evaluator.experimentName, - description: evaluator.description, - metadata: evaluator.metadata, - tags: evaluator.tags, - isPublic: evaluator.isPublic, - update: evaluator.update, - baseExperiment: evaluator.baseExperimentName ?? defaultBaseExperiment, - baseExperimentId: evaluator.baseExperimentId, - gitMetadataSettings: evaluator.gitMetadataSettings, - repoInfo: evaluator.repoInfo, - dataset: Dataset.isDataset(data) ? data : undefined, - parameters, - setCurrent: false, - }); - const info = await logger.summarize({ summarizeScores: false }); - const linkText = info.experimentUrl - ? terminalLink(info.experimentUrl, info.experimentUrl, { - fallback: (_text: string, url: string) => url, - }) - : "locally"; - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - styleText("cyan", "▶") + - ` Experiment ${styleText("bold", info.experimentName)} is running at ${linkText}`, - ); - return logger; -} - -async function getExperimentParametersRef( - parameters: - | Record - | RemoteEvalParameters - | Promise> - | undefined, -): Promise { - if (!parameters) { - return undefined; - } - - const resolvedParameters = - parameters instanceof Promise ? await parameters : parameters; - - if (!RemoteEvalParameters.isParameters(resolvedParameters)) { - return undefined; - } - - if (resolvedParameters.id === undefined) { - return undefined; - } - - return { - id: resolvedParameters.id, - version: resolvedParameters.version, - }; -} - -function resolveReporter( - reporter: string | ReporterDef | undefined, - reporters: Record>, -) { - if (typeof reporter === "string") { - if (!reporters[reporter]) { - throw new Error(`Reporter ${reporter} not found`); - } - return reporters[reporter]; - } else if (!isEmpty(reporter)) { - return reporter; - } else if (Object.keys(reporters).length === 0) { - return fancyReporter; - } else if (Object.keys(reporters).length === 1) { - return reporters[Object.keys(reporters)[0]]; - } else { - const reporterNames = Object.keys(reporters).join(", "); - throw new Error( - `Multiple reporters found (${reporterNames}). Please specify a reporter explicitly.`, - ); - } -} - -type AllReports = Record< - string, - { - reporter: ReporterDef; - results: (unknown | Promise)[]; - } ->; - -function addReport( - evalReports: AllReports, - reporter: ReporterDef, - report: unknown, -) { - if (!evalReports[reporter.name]) { - evalReports[reporter.name] = { - reporter, - results: [], - }; - } - evalReports[reporter.name].results.push(report); -} - -function buildWatchPluginForEvaluator( - inFile: string, - opts: EvaluatorOpts, -): esbuild.Plugin { - const evaluators: EvaluatorState = { - evaluators: [], - reporters: {}, - }; - const plugin = { - name: "run-evalutator-on-end", - setup(build: esbuild.PluginBuild) { - build.onEnd(async (result) => { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(`Done building ${inFile}`); - - if (!result.outputFiles) { - if (opts.showDetailedErrors) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn(`Failed to compile ${inFile}`); - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn(result.errors); - } else { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn(`Failed to compile ${inFile}: ${result.errors}`); - } - return; - } - - const evalResult = evaluateBuildResults(inFile, result); - if (!evalResult) { - return; - } - - evaluators.evaluators = evaluators.evaluators.filter( - (e) => e.sourceFile !== inFile, - ); - - // Update the evaluators and reporters - for (const evaluator of Object.values(evalResult.evaluators)) { - evaluators.evaluators.push({ - sourceFile: inFile, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - evaluator: evaluator.evaluator as EvaluatorDef< - unknown, - unknown, - unknown, - BaseMetadata - >, - reporter: evaluator.reporter, - }); - } - for (const [reporterName, reporter] of Object.entries( - evalResult.reporters, - )) { - evaluators.reporters[reporterName] = reporter; - } - - const evalReports: Record< - string, - { - reporter: ReporterDef; - results: unknown[]; - } - > = {}; - for (const evaluatorDef of Object.values(evalResult.evaluators)) { - const { evaluator, reporter } = evaluatorDef; - const evalData = callEvaluatorData(evaluator.data); - const logger = opts.noSendLogs - ? null - : await initExperiment(evaluator, evalData); - const evaluatorResult = await runEvaluator( - logger, - { - ...evaluator, - data: evalData.data, - }, - opts.progressReporter, - opts.filters, - undefined, - undefined, - ); - const resolvedReporter = resolveReporter( - reporter, - evaluators.reporters, // Let these accumulate across all files. - ); - - const report = resolvedReporter.reportEval( - evaluator, - evaluatorResult, - { - verbose: opts.showDetailedErrors, - jsonl: opts.jsonl, - }, - ); - - addReport(evalReports, resolvedReporter, report); - } - - for (const [reporterName, { reporter, results }] of Object.entries( - evalReports, - )) { - const success = await reporter.reportRun(await Promise.all(results)); - if (!success) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(error(`Reporter ${reporterName} failed.`)); - } - } - }); - }, - }; - - return plugin; -} - -async function initFile({ - inFile, - outFile, - bundleFile, - tsconfig, - plugins, - externalPackages, -}: { - inFile: string; - outFile: string; - bundleFile: string; - tsconfig?: string; - plugins?: PluginMaker[]; - externalPackages?: string[]; -}): Promise { - const buildOptions = buildOpts({ - fileName: inFile, - outFile, - tsconfig, - plugins, - externalPackages, - }); - const ctx = await esbuild.context(buildOptions); - - return { - inFile, - outFile, - bundleFile, - rebuild: async () => { - try { - const result = await ctx.rebuild(); - if (!result.outputFiles) { - return { - type: "failure", - error: new Error("No output file generated"), - sourceFile: inFile, - }; - } - const evaluator = evaluateBuildResults(inFile, result) || { - functions: [], - prompts: [], - parameters: [], - evaluators: {}, - reporters: {}, - }; - return { type: "success", result, evaluator, sourceFile: inFile }; - } catch (e) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { type: "failure", error: e as Error, sourceFile: inFile }; - } - }, - bundle: async () => { - const buildOptions: esbuild.BuildOptions = { - ...buildOpts({ - fileName: inFile, - outFile: bundleFile, - tsconfig, - plugins: [], - externalPackages, - }), - external: ["fsevents", "chokidar"], - write: true, - minify: true, - sourcemap: true, - }; - return await esbuild.build(buildOptions); - }, - watch: () => { - ctx.watch(); - }, - destroy: async () => { - await ctx.dispose(); - }, - }; -} - -interface EvaluatorOpts { - showDetailedErrors: boolean; - apiKey?: string; - orgName?: string; - appUrl?: string; - noSendLogs: boolean; - bundle: boolean; - setCurrent: boolean; - terminateOnFailure: boolean; - watch: boolean; - list: boolean; - jsonl: boolean; - filters: Filter[]; - progressReporter: ProgressReporter; -} - -export function handleBuildFailure({ - result, - terminateOnFailure, - showDetailedErrors, -}: { - result: BuildFailure; - terminateOnFailure: boolean; - showDetailedErrors: boolean; -}) { - if (terminateOnFailure) { - throw result.error; - } else if (showDetailedErrors) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn(`Failed to compile ${result.sourceFile}`); - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn(result.error); - } else { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - `Failed to compile ${result.sourceFile}: ${result.error.message}`, - ); - } -} - -function updateEvaluators( - evaluators: EvaluatorState, - buildResults: BtBuildResult[], - opts: EvaluatorOpts, -) { - for (const result of buildResults) { - if (result.type === "failure") { - handleBuildFailure({ - result, - terminateOnFailure: opts.terminateOnFailure, - showDetailedErrors: opts.showDetailedErrors, - }); - continue; - } - - for (const evaluator of Object.values(result.evaluator.evaluators)) { - evaluators.evaluators.push({ - sourceFile: result.sourceFile, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - evaluator: evaluator.evaluator as EvaluatorDef< - unknown, - unknown, - unknown, - BaseMetadata - >, - reporter: evaluator.reporter, - }); - } - - for (const [reporterName, reporter] of Object.entries( - result.evaluator.reporters, - )) { - if ( - evaluators.reporters[reporterName] && - evaluators.reporters[reporterName] !== reporter - ) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Reporter '${reporterName}' already exists. Will skip '${reporterName}' from ${result.sourceFile}.`, - ), - ); - continue; - } - evaluators.reporters[reporterName] = reporter; - } - } -} - -async function runAndWatch({ - handles, - onExit, -}: { - handles: Record; - onExit?: () => void; -}) { - const count = Object.keys(handles).length; - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(`Watching ${pluralize("file", count, true)}...`); - - Object.values(handles).map((handle) => handle.watch()); - - ["SIGINT", "SIGTERM"].forEach((signal: string) => { - process.on(signal, function () { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error("Stopped watching."); - for (const handle of Object.values(handles)) { - handle.destroy(); - } - onExit?.(); - process.exit(0); - }); - }); - - // Wait forever while we watch. - await new Promise(() => {}); -} - -export async function buildEvaluators( - handles: Record, - opts: EvaluatorOpts, -): Promise<{ evaluators: EvaluatorState; buildResults: BtBuildResult[] }> { - const buildPromises = Object.values(handles).map((handle) => - handle.rebuild(), - ); - - const buildResults = await Promise.all(buildPromises); - - const evaluators: EvaluatorState = { - evaluators: [], - reporters: {}, - }; - updateEvaluators(evaluators, buildResults, opts); - return { evaluators, buildResults }; -} - -async function runOnce( - handles: Record, - opts: EvaluatorOpts, -) { - const bundlePromises = opts.bundle - ? Object.fromEntries( - Object.entries(handles).map(([inFile, handle]) => [ - inFile, - handle.bundle(), - ]), - ) - : null; - - const { evaluators, buildResults } = await buildEvaluators(handles, opts); - - if (opts.list) { - for (const evaluator of evaluators.evaluators) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.log(evaluator.evaluator.evalName); - } - return true; - } - - // map from file name -> eval name -> experiment - const evalToExperiment: Record> = {}; - - const resultPromises = evaluators.evaluators.map(async (evaluator) => { - const evalData = callEvaluatorData(evaluator.evaluator.data); - // TODO: For now, use the eval name as the project. However, we need to evolve - // the definition of a project and create a new concept called run, so that we - // can name the experiment/evaluation within the run the evaluator's name. - const logger = opts.noSendLogs - ? null - : await initExperiment(evaluator.evaluator, evalData); - try { - return await runEvaluator( - logger, - { - ...evaluator.evaluator, - data: evalData.data, - }, - opts.progressReporter, - opts.filters, - undefined, - undefined, - ); - } finally { - if (logger) { - if (!evalToExperiment[evaluator.sourceFile]) { - evalToExperiment[evaluator.sourceFile] = {}; - } - evalToExperiment[evaluator.sourceFile][evaluator.evaluator.evalName] = - logger; - - await logger.flush(); - } - } - }); - - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - styleText( - "dim", - `Processing ${styleText("bold", String(resultPromises.length))} evaluator${resultPromises.length === 1 ? "" : "s"}...`, - ), - ); - const allEvalsResults = await Promise.all(resultPromises); - opts.progressReporter.stop(); - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(""); - - const evalReports: Record< - string, - { - reporter: ReporterDef; - results: unknown[]; - } - > = {}; - for (let idx = 0; idx < evaluators.evaluators.length; idx++) { - const evaluator = evaluators.evaluators[idx]; - const resolvedReporter = resolveReporter( - evaluator.reporter, - evaluators.reporters, - ); - - const report = resolvedReporter.reportEval( - evaluator.evaluator, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - allEvalsResults[idx as number], - { - verbose: opts.showDetailedErrors, - jsonl: opts.jsonl, - }, - ); - - addReport(evalReports, resolvedReporter, report); - } - - if (bundlePromises !== null && Object.entries(evalToExperiment).length > 0) { - await uploadHandleBundles({ - buildResults: buildResults.filter( - // We handle errors above, so it's fine to filter down to successes here. - (result): result is BuildSuccess => result.type === "success", - ), - evalToExperiment, - bundlePromises, - handles, - setCurrent: opts.setCurrent, - defaultIfExists: "replace", - showDetailedErrors: opts.showDetailedErrors, - }); - } - - let allSuccess = true; - for (const [_reporterName, { reporter, results }] of Object.entries( - evalReports, - )) { - const success = await reporter.reportRun(await Promise.all(results)); - allSuccess = allSuccess && success; - } - - return allSuccess; -} - -function checkMatch( - pathInput: string, - include_patterns: string[] | null, - exclude_patterns: string[] | null, -): boolean { - const p = path.resolve(pathInput); - if (include_patterns !== null) { - let include = false; - for (const pattern of include_patterns) { - if (minimatch(p, pattern)) { - include = true; - break; - } - } - if (!include) { - return false; - } - } - - if (exclude_patterns !== null) { - let exclude = false; - for (const pattern of exclude_patterns) { - if (minimatch(p, pattern)) { - exclude = true; - break; - } - } - - return !exclude; - } - - return true; -} - -async function collectFiles( - inputPath: string, - mode: "eval" | "bundle", -): Promise { - let pathStat = null; - try { - pathStat = fs.lstatSync(inputPath); - } catch (e) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(error(`Error reading ${inputPath}: ${e}`)); - process.exit(1); - } - - let files: string[] = []; - if (!pathStat.isDirectory()) { - if ( - !checkMatch( - inputPath, - mode === "eval" ? INCLUDE_EVAL : INCLUDE_BUNDLE, - EXCLUDE, - ) - ) { - const prefix = mode === "eval" ? ".eval" : ""; - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Reading ${inputPath} because it was specified directly. Rename it to end in ${prefix}.ts or ` + - `${prefix}.js to include it automatically when you specify a directory.`, - ), - ); - } - files.push(inputPath); - } else { - const walked = await util.promisify(fsWalk.walk)(inputPath, { - deepFilter: (entry) => { - return checkMatch(entry.path, null, EXCLUDE); - }, - entryFilter: (entry) => { - return ( - entry.dirent.isFile() && - checkMatch( - entry.path, - mode === "eval" ? INCLUDE_EVAL : INCLUDE_BUNDLE, - EXCLUDE, - ) - ); - }, - }); - - files = files.concat(walked.map((entry) => entry.path)); - } - - return files; -} - -import { createMarkKnownPackagesExternalPlugin } from "./util/external-packages-plugin"; - -// Inspired by and modified from https://github.com/evanw/esbuild/issues/1051 -const nativeNodeModulesPlugin = { - name: "native-node-modules", - setup(build: esbuild.PluginBuild) { - // Keep track of packages that contain .node files - const nativePackages = new Set(); - - // Helper to add a package and its platform-specific variants - const addNativePackage = (pkgName: string) => { - nativePackages.add(pkgName); - if (pkgName.includes("@")) { - const [scope, name] = pkgName.split("/"); - const platformPkgs = [ - `${scope}/${name}-darwin-arm64`, - `${scope}/${name}-darwin-x64`, - `${scope}/${name}-linux-x64-gnu`, - `${scope}/${name}-win32-x64-msvc`, - ]; - platformPkgs.forEach((pkg) => nativePackages.add(pkg)); - } - }; - - // When a .node file is imported, mark its package as native - build.onResolve({ filter: /\.node$/ }, (args) => { - try { - const path = require.resolve(args.path, { paths: [args.resolveDir] }); - const match = path.match( - /node_modules[/\\]((?:@[^/\\]+[/\\])?[^/\\]+)/, - ); - if (match) { - addNativePackage(match[1]); - } - } catch { - // Ignore errors - } - return { path: args.path, external: true }; - }); - - // Handle direct imports of native packages - build.onResolve( - { filter: /@[^/]+\/[^/]+-(?:darwin|linux|win32)/ }, - (args) => { - const match = args.path.match(/^(@[^/]+\/[^/]+)/); - if (match) { - addNativePackage(match[1]); - } - return { path: args.path, external: true }; - }, - ); - - // Mark all imports from native packages as external - build.onResolve({ filter: /.*/ }, (args) => { - if (!args.path.startsWith(".") && !args.path.startsWith("/")) { - const match = args.path.match(/^(?:@[^/]+\/)?[^/]+/); - if (match && nativePackages.has(match[0])) { - return { path: require.resolve(args.path), external: true }; - } - } - return null; - }); - }, -}; - -export type PluginMaker = (fileName: string) => esbuild.Plugin; - -function buildOpts({ - fileName, - outFile, - tsconfig, - plugins: argPlugins, - externalPackages, -}: { - fileName: string; - outFile: string; - tsconfig?: string; - plugins?: PluginMaker[]; - externalPackages?: string[]; -}): esbuild.BuildOptions { - const plugins = [ - braintrustEsbuildPlugin(), - nativeNodeModulesPlugin, - createMarkKnownPackagesExternalPlugin(externalPackages), - ...(argPlugins || []).map((fn) => fn(fileName)), - ]; - return { - entryPoints: [fileName], - bundle: true, - treeShaking: true, - outfile: outFile, - platform: "node", - write: false, - // Remove the leading "v" from process.version - target: `node${process.version.slice(1)}`, - tsconfig, - external: ["node_modules/*", "fsevents"], - plugins: plugins, - }; -} - -export async function initializeHandles({ - files: inputFiles, - mode, - plugins, - tsconfig, - externalPackages, -}: { - files: string[]; - mode: "eval" | "bundle"; - plugins?: PluginMaker[]; - tsconfig?: string; - externalPackages?: string[]; -}): Promise> { - const files: Record = {}; - const inputPaths = inputFiles.length > 0 ? inputFiles : ["."]; - for (const inputPath of inputPaths) { - const newFiles = await collectFiles(inputPath, mode); - if (newFiles.length == 0) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Provided path ${inputPath} is not an eval file or a directory containing eval files, skipping...`, - ), - ); - } - for (const file of newFiles) { - files[path.resolve(file)] = true; - } - } - - if (Object.keys(files).length == 0) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning("No eval files were found in any of the provided paths."), - ); - process.exit(0); - } - - const tmpDir = path.join(os.tmpdir(), `btevals-${uuidv4().slice(0, 8)}`); - // fs.mkdirSync(tmpDir, { recursive: true }); - - const initPromises = []; - for (const file of Object.keys(files)) { - const baseName = `${path.basename( - file, - path.extname(file), - )}-${uuidv4().slice(0, 8)}`; - const outFile = path.join(tmpDir, `${baseName}.${OUT_EXT}`); - const bundleFile = path.join(tmpDir, `${baseName}.bundle.js`); - initPromises.push( - initFile({ - inFile: file, - outFile, - bundleFile, - plugins, - tsconfig, - externalPackages, - }), - ); - } - - const handles: Record = {}; - const initResults = await Promise.all(initPromises); - for (const result of initResults) { - handles[result.inFile] = result; - } - return handles; -} - -async function run(args: RunArgs) { - normalizeDebugLoggingArgs(args); - // Load the environment variables from the .env files using the same rules as Next.js - loadEnvConfig(process.cwd(), true); - - if (args.env_file) { - // Load via dotenv library - const loaded = dotenv.config({ path: args.env_file }); - if (loaded.error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(error(`Error loading ${args.env_file}: ${loaded.error}`)); - process.exit(1); - } - } - - const evaluatorOpts: EvaluatorOpts = { - showDetailedErrors: shouldShowDetailedErrors(args.debug_logging), - apiKey: args.api_key, - orgName: args.org_name, - appUrl: args.app_url, - noSendLogs: !!args.no_send_logs, - bundle: !!args.bundle || !!args.push, - setCurrent: !!args.push, - terminateOnFailure: !!args.terminate_on_failure, - watch: !!args.watch, - jsonl: args.jsonl, - progressReporter: args.no_progress_bars - ? new SimpleProgressReporter() - : new BarProgressReporter(), - filters: args.filter ? parseFilters(args.filter) : [], - list: !!args.list, - }; - - if (args.list && args.watch) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(error("Cannot specify both --list and --watch.")); - process.exit(1); - } - - const plugins = evaluatorOpts.watch - ? [ - (fileName: string) => - buildWatchPluginForEvaluator(fileName, evaluatorOpts), - ] - : []; - - const handles = await initializeHandles({ - files: args.files, - mode: "eval", - tsconfig: args.tsconfig, - plugins, - externalPackages: args.external_packages, - }); - - if (args.dev) { - // XXX We should watch these files (or support a --watch flag). - const { evaluators } = await buildEvaluators(handles, evaluatorOpts); - const allEvaluators = Object.values(evaluators.evaluators).map( - (e) => e.evaluator, - ); - runDevServer(allEvaluators, { - host: args.dev_host, - port: args.dev_port, - orgName: args.dev_org_name, - }); - return; - } - - let success = true; - try { - if (!evaluatorOpts.noSendLogs) { - await login({ - apiKey: args.api_key, - orgName: args.org_name, - appUrl: args.app_url, - debugLogLevel: args.debug_logging, - }); - } - - if (args.watch) { - await runAndWatch({ - handles, - onExit: () => { - evaluatorOpts.progressReporter.stop(); - }, - }); - } else { - success = await runOnce(handles, evaluatorOpts); - } - } finally { - // ESBuild can freeze up if you do not clean up the handles properly - for (const handle of Object.values(handles)) { - await handle.destroy(); - } - } - - if (!success) { - process.exit(1); - } -} - -function addAuthArgs(parser: ArgumentParser) { - parser.add_argument("--api-key", { - help: "Specify a braintrust api key. If the parameter is not specified, BRAINTRUST_API_KEY or the nearest .env.braintrust file will be used.", - }); - parser.add_argument("--org-name", { - help: "The name of a specific organization to connect to. This is useful if you belong to multiple.", - }); - parser.add_argument("--app-url", { - help: "Specify a custom braintrust app url. Defaults to https://www.braintrust.dev. This is only necessary if you are using an experimental version of Braintrust", - }); - parser.add_argument("--env-file", { - help: "A path to a .env file containing environment variables to load (via dotenv).", - }); -} - -function addDebugLoggingArg(parser: ArgumentParser) { - parser.add_argument("--debug-logging", { - choices: ["error", "warn", "info", "debug"], - help: "Enable internal Braintrust SDK troubleshooting output. Use 'error', 'warn', 'info', or 'debug' to control the log level.", - }); -} - -function addCompileArgs(parser: ArgumentParser) { - parser.add_argument("--terminate-on-failure", { - action: "store_true", - help: "If provided, terminates on a failing eval, instead of the default (moving onto the next one).", - }); - parser.add_argument("--tsconfig", { - help: "Specify a custom tsconfig.json file to use.", - }); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling. These packages will not be included in the bundle and must be available at runtime. Use this to resolve bundling errors with native modules or problematic dependencies. Example: --external-packages sqlite3 fsevents @mapbox/node-pre-gyp", - }); -} - -async function main() { - const parser = new ArgumentParser({ - description: "Braintrust CLI", - }); - - parser.add_argument("-v", "--version", { action: "version", version }); - - const parentParser = new ArgumentParser({ add_help: false }); - parentParser.add_argument("--verbose", { - action: "store_true", - help: "Deprecated alias for --debug-logging debug. Use --debug-logging debug to include full stack traces and detailed troubleshooting output.", - }); - - const subparser = parser.add_subparsers({ - required: true, - }); - - const parser_run = subparser.add_parser("eval", { - help: "Run evals locally.", - parents: [parentParser], - }); - addAuthArgs(parser_run); - addDebugLoggingArg(parser_run); - parser_run.add_argument("--filter", { - help: "Only run evaluators that match these filters. Each filter is a regular expression (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp). For example, --filter metadata.priority='^P0$' input.name='foo.*bar' will only run evaluators that have metadata.priority equal to 'P0' and input.name matching the regular expression 'foo.*bar'.", - nargs: "*", - }); - parser_run.add_argument("--list", { - help: "List, but do not execute, evaluators.", - action: "store_true", - }); - parser_run.add_argument("--jsonl", { - action: "store_true", - help: "Format score summaries as jsonl, i.e. one JSON-formatted line per summary.", - }); - addCompileArgs(parser_run); - parser_run.add_argument("--watch", { - action: "store_true", - help: "Watch files for changes and rerun evals when changes are detected", - }); - parser_run.add_argument("--no-send-logs", { - action: "store_true", - help: "Do not send logs to Braintrust. Useful for testing evaluators without uploading results.", - }); - parser_run.add_argument("--no-progress-bars", { - action: "store_true", - help: "Do not show progress bars when processing evaluators.", - }); - parser_run.add_argument("--bundle", { - action: "store_true", - help: "Experimental (do not use unless you know what you're doing)", - }); - parser_run.add_argument("--push", { - action: "store_true", - help: "Push the scorers from the current run to Braintrust. This will mark the current run's scorers as the latest version in the project.", - }); - parser_run.add_argument("files", { - nargs: "*", - help: "A list of files or directories to run. If no files are specified, the current directory is used.", - }); - parser_run.add_argument("--dev", { - action: "store_true", - help: "Run the evaluators in dev mode. This will start a dev server which you can connect to via the playground's remote evals feature.", - }); - parser_run.add_argument("--dev-host", { - help: "The host to bind the dev server to. Defaults to localhost. Set to 0.0.0.0 to bind to all interfaces.", - type: String, - default: "localhost", - }); - parser_run.add_argument("--dev-port", { - help: "The port to bind the dev server to. Defaults to 8300.", - type: Number, - default: 8300, - }); - parser_run.add_argument("--dev-org-name", { - help: "Only allow users that belong this to this org name to run remote evals.", - type: String, - }); - parser_run.set_defaults({ func: run }); - - const parser_push = subparser.add_parser("push", { - help: "Bundle prompts, tools, scorers, and other resources into Braintrust", - }); - addAuthArgs(parser_push); - addDebugLoggingArg(parser_push); - addCompileArgs(parser_push); - parser_push.add_argument("files", { - nargs: "*", - help: "A list of files or directories containing functions to bundle. If no files are specified, the current directory is used.", - }); - parser_push.add_argument("--if-exists", { - choices: ["error", "replace", "ignore"], - default: "error", - help: "What to do if a function with the same slug already exists. 'error' will cause an error and abort. 'replace' will overwrite the existing function. 'ignore' will ignore the push for this function and continue.", - }); - parser_push.set_defaults({ func: bundleCommand }); - - const parser_pull = subparser.add_parser("pull", { - help: "Pull prompts, tools, scorers, and other resources from Braintrust to save in your codebase.", - }); - addDebugLoggingArg(parser_pull); - parser_pull.add_argument("--output-dir", { - help: "The directory to output the pulled resources to. If not specified, the current directory is used.", - }); - parser_pull.add_argument("--project-name", { - help: "The name of the project to pull from. If not specified, all projects are pulled.", - }); - parser_pull.add_argument("--project-id", { - help: "The id of the project to pull from. If not specified, all projects are pulled.", - }); - parser_pull.add_argument("--id", { - help: "The id of a specific function to pull.", - }); - parser_pull.add_argument("--slug", { - help: "The slug of a specific function to pull.", - }); - parser_pull.add_argument("--version", { - help: "The version to pull. Will pull the latest version of each prompt that is at or before this version.", - }); - parser_pull.add_argument("--force", { - action: "store_true", - help: "Overwrite local files if they have uncommitted changes.", - }); - parser_pull.set_defaults({ func: pullCommand }); - - const parsed = normalizeDebugLoggingArgs(parser.parse_args()); - - try { - await parsed.func(parsed); - } catch (e) { - logError(e, shouldShowDetailedErrors(parsed.debug_logging)); - process.exit(1); - } -} - -if (require.main === module) { - void main(); -} diff --git a/js/src/cli/integration.test.ts b/js/src/cli/integration.test.ts deleted file mode 100644 index ae20ec41f..000000000 --- a/js/src/cli/integration.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { CompileArgs, RunArgs, BundleArgs, PullArgs } from "./util/types"; - -// Test the type definitions for external packages -describe("External Packages Type Definitions", () => { - describe("CompileArgs interface", () => { - it("should have external_packages property", () => { - const args: CompileArgs = { - tsconfig: "tsconfig.json", - terminate_on_failure: false, - external_packages: ["sqlite3", "fsevents"], - }; - - expect(args.external_packages).toEqual(["sqlite3", "fsevents"]); - }); - - it("should allow undefined external_packages", () => { - const args: CompileArgs = { - tsconfig: "tsconfig.json", - terminate_on_failure: false, - external_packages: undefined, - }; - - expect(args.external_packages).toBeUndefined(); - }); - - it("should allow empty external_packages array", () => { - const args: CompileArgs = { - tsconfig: "tsconfig.json", - terminate_on_failure: false, - external_packages: [], - }; - - expect(args.external_packages).toEqual([]); - }); - }); - - describe("RunArgs interface", () => { - it("should inherit external_packages from CompileArgs", () => { - const args: RunArgs = { - verbose: false, - api_key: "test-key", - org_name: "test-org", - app_url: "https://test.com", - env_file: ".env", - debug_logging: "info", - tsconfig: "tsconfig.json", - terminate_on_failure: false, - external_packages: ["@mapbox/node-pre-gyp"], - files: ["test.eval.ts"], - watch: false, - list: false, - jsonl: false, - filter: undefined, - no_send_logs: false, - no_progress_bars: false, - bundle: false, - push: false, - dev: false, - dev_host: "localhost", - dev_port: 8300, - }; - - expect(args.external_packages).toEqual(["@mapbox/node-pre-gyp"]); - expect(args.debug_logging).toBe("info"); - }); - }); - - describe("BundleArgs interface", () => { - it("should inherit external_packages from CompileArgs", () => { - const args: BundleArgs = { - verbose: false, - api_key: "test-key", - org_name: "test-org", - app_url: "https://test.com", - env_file: ".env", - debug_logging: "debug", - tsconfig: "tsconfig.json", - terminate_on_failure: false, - external_packages: ["sqlite3", "sharp"], - files: ["functions.ts"], - if_exists: "error", - }; - - expect(args.external_packages).toEqual(["sqlite3", "sharp"]); - expect(args.debug_logging).toBe("debug"); - }); - }); - - describe("PullArgs interface", () => { - it("should include debug_logging from CommonArgs", () => { - const args: PullArgs = { - verbose: false, - output_dir: "./braintrust", - project_name: "test-project", - force: false, - debug_logging: "info", - }; - - expect(args.debug_logging).toBe("info"); - }); - }); -}); - -// Test the buildOpts function parameter flow -describe("buildOpts Function Integration", () => { - // This is a simplified version of the buildOpts function signature - interface BuildOptsParams { - fileName: string; - outFile: string; - tsconfig?: string; - plugins?: any[]; - externalPackages?: string[]; - } - - it("should accept externalPackages parameter", () => { - const params: BuildOptsParams = { - fileName: "test.ts", - outFile: "test.js", - tsconfig: "tsconfig.json", - plugins: [], - externalPackages: ["sqlite3", "fsevents"], - }; - - expect(params.externalPackages).toEqual(["sqlite3", "fsevents"]); - }); - - it("should handle undefined externalPackages", () => { - const params: BuildOptsParams = { - fileName: "test.ts", - outFile: "test.js", - tsconfig: "tsconfig.json", - plugins: [], - externalPackages: undefined, - }; - - expect(params.externalPackages).toBeUndefined(); - }); - - it("should handle empty externalPackages array", () => { - const params: BuildOptsParams = { - fileName: "test.ts", - outFile: "test.js", - tsconfig: "tsconfig.json", - plugins: [], - externalPackages: [], - }; - - expect(params.externalPackages).toEqual([]); - }); -}); - -// Test the parameter flow from CLI to buildOpts -describe("Parameter Flow Integration", () => { - it("should flow external_packages from RunArgs to buildOpts", () => { - // Simulate the parameter flow: RunArgs -> initializeHandles -> initFile -> buildOpts - const runArgs: Partial = { - external_packages: ["sqlite3", "@mapbox/node-pre-gyp"], - }; - - // This would be passed to initializeHandles - const initializeHandlesParams = { - files: ["test.eval.ts"], - mode: "eval" as const, - tsconfig: undefined, - plugins: undefined, - externalPackages: runArgs.external_packages, - }; - - expect(initializeHandlesParams.externalPackages).toEqual([ - "sqlite3", - "@mapbox/node-pre-gyp", - ]); - - // This would be passed to initFile - const initFileParams = { - inFile: "test.eval.ts", - outFile: "test.js", - bundleFile: "test.bundle.js", - tsconfig: undefined, - plugins: undefined, - externalPackages: initializeHandlesParams.externalPackages, - }; - - expect(initFileParams.externalPackages).toEqual([ - "sqlite3", - "@mapbox/node-pre-gyp", - ]); - - // This would be passed to buildOpts - const buildOptsParams = { - fileName: initFileParams.inFile, - outFile: initFileParams.outFile, - tsconfig: initFileParams.tsconfig, - plugins: initFileParams.plugins, - externalPackages: initFileParams.externalPackages, - }; - - expect(buildOptsParams.externalPackages).toEqual([ - "sqlite3", - "@mapbox/node-pre-gyp", - ]); - }); - - it("should flow external_packages from BundleArgs to buildOpts", () => { - // Simulate the parameter flow: BundleArgs -> initializeHandles -> initFile -> buildOpts - const bundleArgs: Partial = { - external_packages: ["sharp", "canvas"], - }; - - // This would be passed to initializeHandles - const initializeHandlesParams = { - files: ["functions.ts"], - mode: "bundle" as const, - tsconfig: undefined, - plugins: undefined, - externalPackages: bundleArgs.external_packages, - }; - - expect(initializeHandlesParams.externalPackages).toEqual([ - "sharp", - "canvas", - ]); - - // This would be passed to buildOpts - const buildOptsParams = { - fileName: "functions.ts", - outFile: "functions.js", - tsconfig: undefined, - plugins: undefined, - externalPackages: initializeHandlesParams.externalPackages, - }; - - expect(buildOptsParams.externalPackages).toEqual(["sharp", "canvas"]); - }); -}); - -// Test real-world usage scenarios -describe("Real-world Usage Scenarios", () => { - it("should handle autoevals dependency issue scenario", () => { - const args: Partial = { - files: ["autoevals-test.eval.ts"], - external_packages: ["@mapbox/node-pre-gyp", "mock-aws-s3", "aws-sdk"], - }; - - // These packages would be added to the external list - const expectedExternals = [ - // Hardcoded externals - "braintrust", - "autoevals", - "@braintrust/", - "config", - "lightningcss", - "@mapbox/node-pre-gyp", - // CLI-specified externals - ...args.external_packages!, - ]; - - expect(expectedExternals).toContain("@mapbox/node-pre-gyp"); - expect(expectedExternals).toContain("mock-aws-s3"); - expect(expectedExternals).toContain("aws-sdk"); - }); - - it("should handle database packages scenario", () => { - const args: Partial = { - files: ["database-test.eval.ts"], - external_packages: ["sqlite3", "better-sqlite3", "mysql2"], - }; - - expect(args.external_packages).toEqual([ - "sqlite3", - "better-sqlite3", - "mysql2", - ]); - }); - - it("should handle native modules scenario", () => { - const args: Partial = { - files: ["image-processing.eval.ts"], - external_packages: ["sharp", "canvas", "node-sass"], - }; - - expect(args.external_packages).toEqual(["sharp", "canvas", "node-sass"]); - }); -}); diff --git a/js/src/cli/jest/nodeModulesPaths.ts b/js/src/cli/jest/nodeModulesPaths.ts deleted file mode 100644 index f04748e9a..000000000 --- a/js/src/cli/jest/nodeModulesPaths.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * Adapted from: https://github.com/substack/node-resolve - */ - -import * as path from "node:path"; -import { realpathSync } from "node:fs"; - -type NodeModulesPathsOptions = { - moduleDirectory?: Array; - paths?: Array; -}; - -export default function nodeModulesPaths( - basedir: string, - options: NodeModulesPathsOptions, -): Array { - const modules = - options && options.moduleDirectory - ? Array.from(options.moduleDirectory) - : ["node_modules"]; - - // ensure that `basedir` is an absolute path at this point, - // resolving against the process' current working directory - const basedirAbs = path.resolve(basedir); - - let prefix = "/"; - if (/^([A-Za-z]:)/.test(basedirAbs)) { - prefix = ""; - } else if (/^\\\\/.test(basedirAbs)) { - prefix = "\\\\"; - } - - // The node resolution algorithm (as implemented by NodeJS and TypeScript) - // traverses parents of the physical path, not the symlinked path - let physicalBasedir; - try { - physicalBasedir = realpathSync.native(basedirAbs); - } catch { - // realpath can throw, e.g. on mapped drives - physicalBasedir = basedirAbs; - } - - const paths: Array = [physicalBasedir]; - let parsed = path.parse(physicalBasedir); - while (parsed.dir !== paths[paths.length - 1]) { - paths.push(parsed.dir); - parsed = path.parse(parsed.dir); - } - - const dirs = paths.reduce>((dirs, aPath) => { - for (const moduleDir of modules) { - if (path.isAbsolute(moduleDir)) { - if (aPath === basedirAbs && moduleDir) { - dirs.push(moduleDir); - } - } else { - dirs.push(path.join(prefix, aPath, moduleDir)); - } - } - - return dirs; - }, []); - - if (options.paths) { - dirs.push(...options.paths); - } - - return dirs; -} diff --git a/js/src/cli/progress.ts b/js/src/cli/progress.ts deleted file mode 100644 index befab92f6..000000000 --- a/js/src/cli/progress.ts +++ /dev/null @@ -1 +0,0 @@ -export { BarProgressReporter } from "./reporters/progress"; diff --git a/js/src/cli/reporters/eval.ts b/js/src/cli/reporters/eval.ts deleted file mode 100644 index 8f7ff8708..000000000 --- a/js/src/cli/reporters/eval.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { stripVTControlCharacters, styleText } from "node:util"; -import { terminalLink } from "termi-link"; -import Table from "cli-table3"; -import pluralize from "pluralize"; - -import { ExperimentSummary, ScoreSummary, MetricSummary } from "../../logger"; -import type { ReporterDef } from "../../reporters/types"; -import { EvaluatorDef, EvalResultWithSummary } from "../../framework"; -import { isEmpty } from "../../util"; - -function visibleLength(text: string) { - return stripVTControlCharacters(text).length; -} - -function padEndVisible(text: string, targetLength: number) { - return text + " ".repeat(Math.max(0, targetLength - visibleLength(text))); -} - -function formatSummaryBox(content: string) { - const title = styleText("gray", " Experiment summary "); - const lines = content.split("\n"); - const contentWidth = Math.max( - visibleLength(title), - ...lines.map((line) => visibleLength(line) + 2), - ); - - const horizontal = "─"; - const top = - styleText("gray", "╭") + - title + - styleText( - "gray", - horizontal.repeat(contentWidth - visibleLength(title)) + "╮", - ); - const body = lines - .map( - (line) => - `${styleText("gray", "│")} ${padEndVisible(line, contentWidth - 2)} ${styleText("gray", "│")}`, - ) - .join("\n"); - const bottom = styleText("gray", "╰" + horizontal.repeat(contentWidth) + "╯"); - - return top + "\n" + body + "\n" + bottom; -} - -function formatExperimentSummaryFancy(summary: ExperimentSummary) { - let comparisonLine = ""; - if (summary.comparisonExperimentName) { - comparisonLine = `${summary.comparisonExperimentName} ${styleText("gray", "(baseline)")} ← ${summary.experimentName} ${styleText("gray", "(comparison)")}\n\n`; - } - - const tableParts: string[] = []; - - const hasScores = Object.keys(summary.scores).length > 0; - const hasMetrics = Object.keys(summary.metrics ?? {}).length > 0; - const hasComparison = !!summary.comparisonExperimentName; - - if (hasScores || hasMetrics) { - const headers = [styleText("gray", "Name"), styleText("gray", "Value")]; - - if (hasComparison) { - headers.push( - styleText("gray", "Change"), - styleText("gray", "Improvements"), - styleText("gray", "Regressions"), - ); - } - - const combinedTable = new Table({ - head: hasComparison ? headers : [], - style: { head: [], "padding-left": 0, "padding-right": 0, border: [] }, - chars: { - top: "", - "top-mid": "", - "top-left": "", - "top-right": "", - bottom: "", - "bottom-mid": "", - "bottom-left": "", - "bottom-right": "", - left: "", - "left-mid": "", - mid: "", - "mid-mid": "", - right: "", - "right-mid": "", - middle: " ", - }, - colWidths: hasComparison ? [18, 10, 10, 13, 12] : [20, 15], - colAligns: hasComparison - ? ["left", "right", "right", "right", "right"] - : ["left", "right"], - wordWrap: false, - }); - - const scoreValues: ScoreSummary[] = Object.values(summary.scores); - for (const score of scoreValues) { - const scorePercent = (score.score * 100).toFixed(2); - const scoreValue = styleText("white", `${scorePercent}%`); - - let diffString = ""; - if (!isEmpty(score.diff)) { - const diffPercent = (score.diff! * 100).toFixed(2); - const diffSign = score.diff! > 0 ? "+" : ""; - const diffColor = score.diff! > 0 ? "green" : "red"; - diffString = styleText(diffColor, `${diffSign}${diffPercent}%`); - } else { - diffString = styleText("gray", "-"); - } - - const improvements = - score.improvements > 0 - ? styleText(["dim", "green"], String(score.improvements)) - : styleText("gray", "-"); - const regressions = - score.regressions > 0 - ? styleText(["dim", "red"], String(score.regressions)) - : styleText("gray", "-"); - - const row = [`${styleText("blue", "◯")} ${score.name}`, scoreValue]; - if (hasComparison) { - row.push(diffString, improvements, regressions); - } - combinedTable.push(row); - } - - const metricValues: MetricSummary[] = Object.values(summary.metrics ?? {}); - for (const metric of metricValues) { - const fractionDigits = Number.isInteger(metric.metric) ? 0 : 2; - const formattedValue = metric.metric.toFixed(fractionDigits); - const metricValue = styleText( - "white", - metric.unit === "$" - ? `${metric.unit}${formattedValue}` - : `${formattedValue}${metric.unit}`, - ); - - let diffString = ""; - if (!isEmpty(metric.diff)) { - const diffPercent = (metric.diff! * 100).toFixed(2); - const diffSign = metric.diff! > 0 ? "+" : ""; - const diffColor = metric.diff! > 0 ? "green" : "red"; - diffString = styleText(diffColor, `${diffSign}${diffPercent}%`); - } else { - diffString = styleText("gray", "-"); - } - - const improvements = - metric.improvements > 0 - ? styleText(["dim", "green"], String(metric.improvements)) - : styleText("gray", "-"); - const regressions = - metric.regressions > 0 - ? styleText(["dim", "red"], String(metric.regressions)) - : styleText("gray", "-"); - - const row = [`${styleText("magenta", "◯")} ${metric.name}`, metricValue]; - if (hasComparison) { - row.push(diffString, improvements, regressions); - } - combinedTable.push(row); - } - - tableParts.push(combinedTable.toString()); - } - - const content = [comparisonLine, ...tableParts].filter(Boolean).join("\n"); - - const footer = summary.experimentUrl - ? terminalLink( - `View results for ${summary.experimentName}`, - summary.experimentUrl, - { fallback: () => `See results at ${summary.experimentUrl}` }, - ) - : ""; - - const boxContent = [content, footer].filter(Boolean).join("\n\n"); - - return "\n" + formatSummaryBox(boxContent); -} - -export const warning = (text: string) => styleText("yellow", text); - -export const fancyReporter: ReporterDef = { - name: "Braintrust fancy reporter", - async reportEval( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - evaluator: EvaluatorDef, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - result: EvalResultWithSummary, - { verbose, jsonl }: { verbose: boolean; jsonl?: boolean }, - ) { - const { results, summary } = result; - const failingResults = results.filter( - (r: { error: unknown }) => r.error !== undefined, - ); - - if (failingResults.length > 0) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - warning( - `Evaluator ${evaluator.evalName} failed with ${pluralize("error", failingResults.length, true)}. This evaluation ("${evaluator.evalName}") will not be fully logged.`, - ), - ); - if (jsonl) { - for (const result of failingResults) { - process.stdout.write(JSON.stringify(result)); - process.stdout.write("\n"); - } - } else if (verbose) { - for (const result of failingResults) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(result); - } - } - } - - process.stdout.write( - jsonl ? JSON.stringify(summary) : formatExperimentSummaryFancy(summary), - ); - process.stdout.write("\n"); - return failingResults.length === 0; - }, - async reportRun(evalReports: boolean[]) { - return evalReports.every((r) => r); - }, -}; diff --git a/js/src/cli/reporters/progress.ts b/js/src/cli/reporters/progress.ts deleted file mode 100644 index e764709f3..000000000 --- a/js/src/cli/reporters/progress.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { styleText } from "node:util"; -import * as cliProgress from "cli-progress"; - -import type { ProgressReporter } from "../../reporters/types"; - -export { SimpleProgressReporter } from "../../reporters/progress"; - -const MAX_NAME_LENGTH = 40; - -function fitNameToSpaces(name: string, length: number) { - const padded = name.padEnd(length); - if (padded.length <= length) { - return padded; - } - return padded.substring(0, length - 3) + "..."; -} - -export class BarProgressReporter implements ProgressReporter { - private multiBar: cliProgress.MultiBar; - private bars: Record = {}; - - constructor() { - this.multiBar = new cliProgress.MultiBar( - { - format: `${styleText("blueBright", "{bar}")} ${styleText("blue", "{evaluator}")} {percentage}% ${styleText("gray", "{value}/{total} {eta_formatted}")}`, - hideCursor: true, - barsize: 10, - }, - cliProgress.Presets.shades_grey, - ); - } - - public start(name: string, total: number) { - const bar = this.multiBar.create(total, 0); - this.bars[name] = bar; - } - - public stop() { - this.multiBar.stop(); - } - - public increment(name: string) { - this.bars[name].increment({ - evaluator: fitNameToSpaces(name, MAX_NAME_LENGTH), - }); - } - - public setTotal(name: string, total: number) { - this.bars[name].setTotal(total); - } -} diff --git a/js/src/cli/types.ts b/js/src/cli/types.ts deleted file mode 100644 index 15184a247..000000000 --- a/js/src/cli/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type * as esbuild from "esbuild"; -import type { BaseMetadata } from "../logger"; -import type { EvaluatorDef, EvaluatorFile } from "../framework"; -import type { ReporterDef } from "../reporters/types"; - -export interface BuildSuccess { - type: "success"; - result: esbuild.BuildResult; - evaluator: EvaluatorFile; - sourceFile: string; -} - -export interface BuildFailure { - type: "failure"; - error: Error; - sourceFile: string; -} - -export type BtBuildResult = BuildSuccess | BuildFailure; - -export interface FileHandle { - inFile: string; - outFile: string; - bundleFile?: string; - rebuild: () => Promise; - bundle: () => Promise; - watch: () => void; - destroy: () => Promise; -} - -export interface EvaluatorState { - evaluators: { - sourceFile: string; - evaluator: EvaluatorDef; - reporter: string | ReporterDef | undefined; - }[]; - reporters: { - [reporter: string]: ReporterDef; - }; -} diff --git a/js/src/cli/util/bundle.ts b/js/src/cli/util/bundle.ts deleted file mode 100644 index 9eb18bced..000000000 --- a/js/src/cli/util/bundle.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { loadEnvConfig } from "@next/env"; -import * as dotenv from "dotenv"; -import { AuthArgs, BundleArgs, CommonArgs } from "./types"; -import { error } from "../../framework"; -import type { BtBuildResult } from "../types"; -import { handleBuildFailure, initializeHandles } from "../index"; -import { login } from "../../logger"; -import { uploadHandleBundles } from "../functions/upload"; -import { - normalizeDebugLoggingArgs, - shouldShowDetailedErrors, -} from "./debug-logging"; - -export async function loadCLIEnv(args: AuthArgs & CommonArgs) { - normalizeDebugLoggingArgs(args); - // Load the environment variables from the .env files using the same rules as Next.js - loadEnvConfig(process.cwd(), true); - - if (args.env_file) { - // Load via dotenv library - const loaded = dotenv.config({ path: args.env_file }); - if (loaded.error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(error(`Error loading ${args.env_file}: ${loaded.error}`)); - process.exit(1); - } - } - - await login({ - apiKey: args.api_key, - orgName: args.org_name, - appUrl: args.app_url, - debugLogLevel: args.debug_logging, - }); -} - -export async function bundleCommand(args: BundleArgs) { - await loadCLIEnv(args); - const showDetailedErrors = shouldShowDetailedErrors(args.debug_logging); - - const handles = await initializeHandles({ - mode: "bundle", - files: args.files, - tsconfig: args.tsconfig, - externalPackages: args.external_packages, - }); - - try { - const allBuildResultsP: Promise[] = Object.values( - handles, - ).map((handle) => handle.rebuild()); - - const bundlePromises = Object.fromEntries( - Object.entries(handles).map(([inFile, handle]) => [ - inFile, - handle.bundle(), - ]), - ); - - const allBuildResults = await Promise.all(allBuildResultsP); - const buildResults = []; - for (const buildResult of allBuildResults) { - if (buildResult.type === "failure") { - handleBuildFailure({ - result: buildResult, - terminateOnFailure: args.terminate_on_failure, - showDetailedErrors, - }); - } else { - buildResults.push(buildResult); - } - } - - const { numFailed } = await uploadHandleBundles({ - buildResults, - bundlePromises, - handles, - setCurrent: true, - showDetailedErrors, - defaultIfExists: args.if_exists, - }); - - if (numFailed > 0) { - process.exit(1); - } - } finally { - for (const handle of Object.values(handles)) { - await handle.destroy(); - } - } -} diff --git a/js/src/cli/util/debug-logging.test.ts b/js/src/cli/util/debug-logging.test.ts deleted file mode 100644 index 320911e5e..000000000 --- a/js/src/cli/util/debug-logging.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; - -import { - normalizeDebugLoggingArgs, - resetDebugLoggingArgsForTests, - shouldShowDetailedErrors, - VERBOSE_DEPRECATION_MESSAGE, -} from "./debug-logging"; - -describe("CLI debug logging helpers", () => { - beforeEach(() => { - vi.restoreAllMocks(); - resetDebugLoggingArgsForTests(); - }); - - test("shouldShowDetailedErrors only enables detailed errors for debug", () => { - expect(shouldShowDetailedErrors(undefined)).toBe(false); - expect(shouldShowDetailedErrors("info")).toBe(false); - expect(shouldShowDetailedErrors("debug")).toBe(true); - }); - - test("normalizeDebugLoggingArgs upgrades verbose to debug and warns once", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - const args = { - verbose: true, - debug_logging: undefined as - | "error" - | "warn" - | "info" - | "debug" - | undefined, - }; - - normalizeDebugLoggingArgs(args); - normalizeDebugLoggingArgs(args); - - expect(args.debug_logging).toBe("debug"); - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy).toHaveBeenCalledWith( - `Warning: ${VERBOSE_DEPRECATION_MESSAGE}`, - ); - }); - - test("normalizeDebugLoggingArgs preserves explicit debug logging", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - const args = { verbose: true, debug_logging: "info" as const }; - - normalizeDebugLoggingArgs(args); - - expect(args.debug_logging).toBe("info"); - expect(warnSpy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/js/src/cli/util/debug-logging.ts b/js/src/cli/util/debug-logging.ts deleted file mode 100644 index 544514a11..000000000 --- a/js/src/cli/util/debug-logging.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { warning } from "../../framework"; -import type { CommonArgs } from "./types"; - -export const VERBOSE_DEPRECATION_MESSAGE = - "--verbose is deprecated and will be removed in a future version of braintrust. Use --debug-logging debug to see full stack traces and troubleshooting details."; - -let hasWarnedAboutVerboseFlag = false; - -export function shouldShowDetailedErrors( - debugLogLevel: CommonArgs["debug_logging"] | undefined, -): boolean { - return debugLogLevel === "debug"; -} - -export function normalizeDebugLoggingArgs< - T extends Pick, ->(args: T): T { - if (!args.verbose) { - return args; - } - - if (!hasWarnedAboutVerboseFlag) { - hasWarnedAboutVerboseFlag = true; - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn(warning(VERBOSE_DEPRECATION_MESSAGE)); - } - - if (!args.debug_logging) { - args.debug_logging = "debug"; - } - - return args; -} - -export function resetDebugLoggingArgsForTests(): void { - hasWarnedAboutVerboseFlag = false; -} diff --git a/js/src/cli/util/external-packages-plugin.test.ts b/js/src/cli/util/external-packages-plugin.test.ts deleted file mode 100644 index 34011d05f..000000000 --- a/js/src/cli/util/external-packages-plugin.test.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { createMarkKnownPackagesExternalPlugin } from "./external-packages-plugin"; - -describe("External Packages Plugin", () => { - describe("createMarkKnownPackagesExternalPlugin", () => { - it("should create a plugin with the correct name", () => { - const plugin = createMarkKnownPackagesExternalPlugin(); - expect(plugin.name).toBe("make-known-packages-external"); - }); - - it("should mark hardcoded packages as external", () => { - const plugin = createMarkKnownPackagesExternalPlugin(); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - expect(mockBuild.onResolve).toHaveBeenCalledWith( - { filter: expect.any(RegExp) }, - expect.any(Function), - ); - - // Get the filter regex that was passed - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - const resolveFunction = mockBuild.onResolve.mock.calls[0][1]; - - // Test hardcoded packages - expect(filter.test("braintrust")).toBe(true); - expect(filter.test("autoevals")).toBe(true); - expect(filter.test("@mapbox/node-pre-gyp")).toBe(true); - expect(filter.test("config")).toBe(true); - expect(filter.test("lightningcss")).toBe(true); - - // Test that the resolve function returns external: true - expect(resolveFunction({ path: "braintrust" })).toEqual({ - path: "braintrust", - external: true, - }); - }); - - it("should not mark non-matching packages as external", () => { - const plugin = createMarkKnownPackagesExternalPlugin(); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Test packages that should NOT match - expect(filter.test("react")).toBe(false); - expect(filter.test("lodash")).toBe(false); - expect(filter.test("@types/node")).toBe(false); - expect(filter.test("some-random-package")).toBe(false); - }); - - it("should include additional packages from CLI", () => { - const additionalPackages = ["sqlite3", "fsevents", "@scope/package"]; - const plugin = createMarkKnownPackagesExternalPlugin(additionalPackages); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Test that additional packages are included - expect(filter.test("sqlite3")).toBe(true); - expect(filter.test("fsevents")).toBe(true); - expect(filter.test("@scope/package")).toBe(true); - - // Test that hardcoded packages still work - expect(filter.test("braintrust")).toBe(true); - expect(filter.test("@mapbox/node-pre-gyp")).toBe(true); - }); - - it("should handle packages with subpaths", () => { - const plugin = createMarkKnownPackagesExternalPlugin(); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Test subpaths - expect(filter.test("braintrust/core")).toBe(true); - expect(filter.test("braintrust/dist/index.js")).toBe(true); - expect( - filter.test("@mapbox/node-pre-gyp/lib/util/nw-pre-gyp/index.html"), - ).toBe(true); - }); - - it("should handle special characters in package names", () => { - const additionalPackages = [ - "@scope/package-with-dashes", - "package.with.dots", - ]; - const plugin = createMarkKnownPackagesExternalPlugin(additionalPackages); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Test packages with special characters - expect(filter.test("@scope/package-with-dashes")).toBe(true); - expect(filter.test("package.with.dots")).toBe(true); - }); - - it("should resolve the original @mapbox/node-pre-gyp bundling issue", () => { - const plugin = createMarkKnownPackagesExternalPlugin(); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - const resolveFunction = mockBuild.onResolve.mock.calls[0][1]; - - // Test the specific problematic paths from the original issue - const problematicPaths = [ - "@mapbox/node-pre-gyp", - "@mapbox/node-pre-gyp/lib/util/nw-pre-gyp/index.html", - "@mapbox/node-pre-gyp/lib/util/s3_setup.js", - "@mapbox/node-pre-gyp/lib/node-pre-gyp.js", - ]; - - problematicPaths.forEach((path) => { - expect(filter.test(path)).toBe(true); - expect(resolveFunction({ path })).toEqual({ - path, - external: true, - }); - }); - }); - - it("should prevent false positives with similar package names", () => { - const plugin = createMarkKnownPackagesExternalPlugin(); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Test cases that should NOT match (false positives) - const falsePositives = [ - "braintrust-extended", - "my-braintrust", - "config-loader", - "lightningcss-plugin", - "sqlite3-wrapper", - "better-sqlite3", - "node-pre-gyp", // Similar to @mapbox/node-pre-gyp but different - "@other/braintrust", - ]; - - falsePositives.forEach((packageName) => { - expect(filter.test(packageName)).toBe(false); - }); - - // Test cases that SHOULD match (true positives) - const truePositives = [ - "braintrust", - "braintrust/core", - "braintrust/dist/index.js", - "@mapbox/node-pre-gyp", - "@mapbox/node-pre-gyp/lib/util/nw-pre-gyp/index.html", - "config", - "lightningcss", - ]; - - truePositives.forEach((packageName) => { - expect(filter.test(packageName)).toBe(true); - }); - }); - - it("should handle prefix matching vs exact matching correctly", () => { - const plugin = createMarkKnownPackagesExternalPlugin(); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Test prefix matching for @braintrust/ (ends with /) - const braintrustPrefixTests = [ - { - input: "@braintrust/utils", - expected: true, - description: "Should match @braintrust/ prefix", - }, - { - input: "@braintrust-other/core", - expected: false, - description: "Should not match similar but different scope", - }, - ]; - - // Test exact matching for config (doesn't end with /) - const configExactTests = [ - { - input: "config", - expected: true, - description: "Should match config exactly", - }, - { - input: "config/local", - expected: true, - description: "Should match config with subpath", - }, - { - input: "config-loader", - expected: false, - description: "Should not match config with suffix", - }, - { - input: "my-config", - expected: false, - description: "Should not match config with prefix", - }, - ]; - - // Test exact matching for braintrust (doesn't end with /) - const braintrustExactTests = [ - { - input: "braintrust", - expected: true, - description: "Should match braintrust exactly", - }, - { - input: "braintrust/core", - expected: true, - description: "Should match braintrust with subpath", - }, - { - input: "braintrust-extended", - expected: false, - description: "Should not match braintrust with suffix", - }, - { - input: "my-braintrust", - expected: false, - description: "Should not match braintrust with prefix", - }, - ]; - - const allTests = [ - ...braintrustPrefixTests, - ...configExactTests, - ...braintrustExactTests, - ]; - - allTests.forEach((test) => { - const result = filter.test(test.input); - expect(result).toBe(test.expected); - }); - }); - }); - - describe("Regex Edge Cases", () => { - it("should handle empty additional packages array", () => { - const plugin = createMarkKnownPackagesExternalPlugin([]); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Should still work with hardcoded packages - expect(filter.test("braintrust")).toBe(true); - expect(filter.test("@mapbox/node-pre-gyp")).toBe(true); - }); - - it("should handle undefined additional packages", () => { - const plugin = createMarkKnownPackagesExternalPlugin(undefined); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Should still work with hardcoded packages - expect(filter.test("braintrust")).toBe(true); - expect(filter.test("@mapbox/node-pre-gyp")).toBe(true); - }); - - it("should handle duplicate packages", () => { - const additionalPackages = ["braintrust", "sqlite3", "braintrust"]; // duplicate - const plugin = createMarkKnownPackagesExternalPlugin(additionalPackages); - const mockBuild = { - onResolve: vi.fn(), - }; - - plugin.setup(mockBuild as any); - - const [{ filter }] = mockBuild.onResolve.mock.calls[0]; - - // Should still work correctly - expect(filter.test("braintrust")).toBe(true); - expect(filter.test("sqlite3")).toBe(true); - }); - }); -}); diff --git a/js/src/cli/util/external-packages-plugin.ts b/js/src/cli/util/external-packages-plugin.ts deleted file mode 100644 index 115a98bff..000000000 --- a/js/src/cli/util/external-packages-plugin.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as esbuild from "esbuild"; - -// Inspired by https://github.com/evanw/esbuild/issues/619 -// In addition to marking node_modules external, explicitly mark -// our packages (braintrust and autoevals) external, in case they're -// installed in a relative path. -export function createMarkKnownPackagesExternalPlugin( - additionalPackages: string[] = [], -) { - return { - name: "make-known-packages-external", - setup(build: esbuild.PluginBuild) { - // Mark known packages as external - const knownPackages = [ - "braintrust", - "autoevals", - "@braintrust/", - "config", - "lightningcss", - "@mapbox/node-pre-gyp", - "fsevents", - "chokidar", - ...additionalPackages, - ]; - const escapedPackages = knownPackages.map((pkg) => { - const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - // For packages ending with /, match anything with that prefix - if (pkg.endsWith("/")) { - return escaped + ".*"; - } - // For regular packages, match exact name or name followed by / - return escaped + "(?:\\/.*)?"; - }); - const knownPackagesFilter = new RegExp( - `^(${escapedPackages.join("|")})$`, - ); - build.onResolve({ filter: knownPackagesFilter }, (args) => ({ - path: args.path, - external: true, - })); - }, - }; -} diff --git a/js/src/cli/util/external-packages.test.ts b/js/src/cli/util/external-packages.test.ts deleted file mode 100644 index 210279917..000000000 --- a/js/src/cli/util/external-packages.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { ArgumentParser } from "argparse"; - -// Test the CLI argument parsing for external packages -describe("External Packages CLI Arguments", () => { - describe("--external-packages flag", () => { - it("should parse single package", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args(["--external-packages", "sqlite3"]); - expect(args.external_packages).toEqual(["sqlite3"]); - }); - - it("should parse multiple packages", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args([ - "--external-packages", - "sqlite3", - "fsevents", - "@mapbox/node-pre-gyp", - ]); - expect(args.external_packages).toEqual([ - "sqlite3", - "fsevents", - "@mapbox/node-pre-gyp", - ]); - }); - - it("should handle scoped packages", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args([ - "--external-packages", - "@scope/package", - "@another/scoped-package", - ]); - expect(args.external_packages).toEqual([ - "@scope/package", - "@another/scoped-package", - ]); - }); - - it("should handle packages with special characters", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args([ - "--external-packages", - "package-with-dashes", - "package.with.dots", - "@scope/package-with-dashes", - ]); - expect(args.external_packages).toEqual([ - "package-with-dashes", - "package.with.dots", - "@scope/package-with-dashes", - ]); - }); - - it("should handle empty flag (no packages)", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args(["--external-packages"]); - expect(args.external_packages).toEqual([]); - }); - - it("should handle flag not being used", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args([]); - expect(args.external_packages).toBeUndefined(); - }); - - it("should handle mixed with other arguments", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - parser.add_argument("--verbose", { - action: "store_true", - help: "Verbose output", - }); - parser.add_argument("files", { - nargs: "*", - help: "Files to process", - }); - - const args = parser.parse_args([ - "--verbose", - "--external-packages", - "sqlite3", - "fsevents", - "--", - "file1.ts", - "file2.ts", - ]); - - expect(args.external_packages).toEqual(["sqlite3", "fsevents"]); - expect(args.verbose).toBe(true); - expect(args.files).toEqual(["file1.ts", "file2.ts"]); - }); - }); - - describe("Real-world scenarios", () => { - it("should handle database packages", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args([ - "--external-packages", - "sqlite3", - "better-sqlite3", - "mysql2", - "pg", - ]); - - expect(args.external_packages).toEqual([ - "sqlite3", - "better-sqlite3", - "mysql2", - "pg", - ]); - }); - - it("should handle native modules", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args([ - "--external-packages", - "sharp", - "canvas", - "node-sass", - "fsevents", - ]); - - expect(args.external_packages).toEqual([ - "sharp", - "canvas", - "node-sass", - "fsevents", - ]); - }); - - it("should handle problematic bundling packages", () => { - const parser = new ArgumentParser(); - parser.add_argument("--external-packages", { - nargs: "*", - help: "Additional packages to mark as external during bundling.", - }); - - const args = parser.parse_args([ - "--external-packages", - "@mapbox/node-pre-gyp", - "mock-aws-s3", - "aws-sdk", - "node-pre-gyp", - ]); - - expect(args.external_packages).toEqual([ - "@mapbox/node-pre-gyp", - "mock-aws-s3", - "aws-sdk", - "node-pre-gyp", - ]); - }); - }); -}); diff --git a/js/src/cli/util/pull.ts b/js/src/cli/util/pull.ts deleted file mode 100644 index 76d214237..000000000 --- a/js/src/cli/util/pull.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { - Function as functionSchema, - type FunctionType as FunctionObject, - type SavedFunctionIdType as SavedFunctionId, - type ToolFunctionDefinitionType as ToolFunctionDefinition, -} from "../../generated_types"; -import { _internalGetGlobalState } from "../../logger"; -import { loadCLIEnv } from "./bundle"; -import { PullArgs } from "./types"; -import { warning } from "../../framework"; -import { z } from "zod/v3"; -import fs from "node:fs/promises"; -import util from "node:util"; -import { slugify } from "../../../util/string_util"; -import path from "node:path"; -import { runGitCommand } from "../../git-command"; -import { currentRepoPath } from "../../gitutil"; -import { isEmpty, loadPrettyXact, prettifyXact } from "../../../util/index"; -import { - ProjectNameIdMap, - toolFunctionDefinitionSchema, -} from "../../framework2"; -import pluralize from "pluralize"; - -export async function pullCommand(args: PullArgs) { - await loadCLIEnv(args); - - const loggerConn = _internalGetGlobalState().apiConn(); - const functions = await loggerConn.get_json("/v1/function", { - ...(args.project_id ? { project_id: args.project_id } : {}), - ...(args.project_name ? { project_name: args.project_name } : {}), - ...(args.slug ? { slug: args.slug } : {}), - ...(args.id ? { ids: [args.id] } : {}), - ...(args.version ? { version: loadPrettyXact(args.version) } : {}), - }); - const functionObjects = z - .object({ objects: z.array(z.unknown()) }) - .parse(functions); - - const projectNameToFunctions: Record = {}; - const projectNameIdMap = new ProjectNameIdMap(); - - for (const rawFunc of functionObjects.objects) { - const parsedFunc = functionSchema.safeParse(rawFunc); - if (!parsedFunc.success) { - const id = - typeof rawFunc === "object" && rawFunc && "id" in rawFunc - ? ` ${rawFunc.id}` - : ""; - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning(`Failed to parse function${id}: ${parsedFunc.error.message}`), - ); - continue; - } - - const func = parsedFunc.data; - const projectName = await projectNameIdMap.getName(func.project_id); - if (!projectNameToFunctions[projectName]) { - projectNameToFunctions[projectName] = []; - } - projectNameToFunctions[projectName].push(func); - } - - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.log("Found functions in the following projects:"); - for (const projectName of Object.keys(projectNameToFunctions)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.log(` * ${projectName}`); - } - - const outputDir = args.output_dir ?? "./braintrust"; - await fs.mkdir(outputDir, { recursive: true }); - - const repoRoot = await currentRepoPath(); - const dirtyFileOutput = repoRoot - ? await runGitCommand(["diff", "--name-only", "-z", "HEAD"], { - cwd: repoRoot, - }) - : ""; - const dirtyFiles = new Set( - dirtyFileOutput - .split("\0") - .filter(Boolean) - .map((file) => path.resolve(repoRoot ?? ".", file)), - ); - - for (const projectName of Object.keys(projectNameToFunctions)) { - const projectFile = path.join( - outputDir, - `${slugify(projectName, { lower: true, strict: true, trim: true })}.ts`, - ); - const resolvedProjectFile = path.resolve(projectFile); - const fileExists = await fs.stat(projectFile).then( - () => true, - () => false, - ); - if (args.force) { - if (fileExists) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Overwriting ${doubleQuote(projectFile)} because --force is set.`, - ), - ); - } - } else if (dirtyFiles.has(resolvedProjectFile)) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Skipping project ${projectName} because ${doubleQuote(projectFile)} has uncommitted changes.`, - ), - ); - continue; - } else if (fileExists) { - if (!repoRoot) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Project ${projectName} already exists in ${doubleQuote(projectFile)}. Skipping since this is not a git repository...`, - ), - ); - continue; - } else { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Project ${projectName} already exists in ${doubleQuote(projectFile)}. Overwriting...`, - ), - ); - } - } - - const projectFileContents = await makeProjectFile({ - projectName, - projectId: await projectNameIdMap.getId(projectName), - fileName: projectFile, - functions: projectNameToFunctions[projectName], - hasSpecifiedFunction: !!args.slug || !!args.id, - }); - await fs.writeFile(projectFile, projectFileContents || ""); - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.log(`Wrote ${projectName} to ${doubleQuote(projectFile)}`); - } -} - -async function makeProjectFile({ - projectName, - projectId, - fileName, - functions, - hasSpecifiedFunction, -}: { - projectName: string; - projectId: string; - fileName: string; - functions: FunctionObject[]; - hasSpecifiedFunction: boolean; -}) { - const varNames = {}; - const functionDefinitions = functions - .map((f) => - makeFunctionDefinition({ func: f, varNames, hasSpecifiedFunction }), - ) - .filter((f) => f !== null); - const fileDef = `// This file was automatically generated by braintrust pull. You can -// generate it again by running: -// $ braintrust pull --project-name ${doubleQuote(projectName)} -// Feel free to edit this file manually, but once you do, you should make sure to -// sync your changes with Braintrust by running: -// $ braintrust push ${doubleQuote(fileName)} - -import braintrust from "braintrust"; - -const project = braintrust.projects.create({ - id: ${doubleQuote(projectId)}, - name: ${doubleQuote(projectName)}, -}); - -${functionDefinitions.join("\n")} -`; - - const prettier = await getPrettierModule(); - if (prettier) { - try { - const formatted = prettier.format(fileDef, { - parser: "typescript", - }); - return formatted; - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Failed to format with prettier (${error instanceof Error ? error.message : error}). Using unformatted output.`, - ), - ); - } - } - return fileDef; -} - -function makeFunctionDefinition({ - func, - varNames, - hasSpecifiedFunction, -}: { - func: FunctionObject; - varNames: Record; - hasSpecifiedFunction: boolean; -}): string | null { - if (func.function_data.type !== "prompt") { - if (hasSpecifiedFunction) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Skipping function ${doubleQuote(func.name)} because it is not a prompt.`, - ), - ); - } - return null; - } - - const baseVarName = slugToVarName(func.slug); - let varName = baseVarName; - let suffix = 1; - while (varName in varNames) { - varName = `${varName}${suffix}`; - suffix++; - } - varNames[varName] = func.slug; - - if (!func.prompt_data || !func.prompt_data.prompt) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Prompt ${doubleQuote(func.name)} has an invalid (empty) prompt definition.`, - ), - ); - return null; - } - const objectType = "prompt"; - const prompt = func.prompt_data.prompt; - const promptContents = - prompt.type === "completion" - ? `prompt: ${doubleQuote(prompt.content)}` - : `messages: ${safeStringify(prompt.messages).trimStart()}`; - - const rawToolsParsed = - prompt.type === "chat" && prompt.tools && prompt.tools.length > 0 - ? z - .array(toolFunctionDefinitionSchema) - .safeParse(JSON.parse(prompt.tools)) - : undefined; - - if (rawToolsParsed && !rawToolsParsed.success) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - `Prompt ${doubleQuote(func.name)} has an invalid tools definition: ${rawToolsParsed.error.message}. Skipping...`, - ), - ); - return null; - } - - const rawTools = rawToolsParsed ? rawToolsParsed.data : []; - - const { model, params } = func.prompt_data.options ?? {}; - - const paramsString = - params && Object.keys(params).length > 0 - ? `params: ${safeStringify(params).trimStart()},` - : ""; - - const tools: (SavedFunctionId | ToolFunctionDefinition)[] = [ - ...(func.prompt_data.tool_functions ?? []), - ...rawTools, - ]; - - const toolsString = - tools.length > 0 ? `tools: ${safeStringify(tools).trimStart()},` : ""; - - return `export const ${varName} = project.${pluralize(objectType)}.create({ - id: ${doubleQuote(func.id)}, - name: ${doubleQuote(func.name)}, - slug: ${doubleQuote(func.slug)}, - version: ${doubleQuote(prettifyXact(func._xact_id))}, ${printOptionalField("description", func.description)}${printOptionalField("model", model)} -${indent(promptContents, 2)}, -${indent(paramsString, 2)} -${indent(toolsString, 2)} -}); -`; -} - -function doubleQuote(str: string) { - return JSON.stringify(str); -} - -function slugToVarName(slug: string) { - let varName = slug.replace(/^[^a-zA-Z_$]|[^a-zA-Z0-9_$]/g, "_"); - varName = varName.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); - varName = varName.charAt(0).toLowerCase() + varName.slice(1); - return varName; -} - -function indent(str: string, numSpaces: number) { - return str.replace(/^/gm, " ".repeat(numSpaces)); -} - -function printOptionalField( - fieldName: string, - fieldValue: string | undefined | null, -) { - return !isEmpty(fieldValue) - ? ` - ${fieldName}: ${doubleQuote(fieldValue)},` - : ""; -} - -let prettierImportAttempted = false; - -let prettierModule: typeof import("prettier") | undefined = undefined; - -async function getPrettierModule() { - if (!prettierModule && !prettierImportAttempted) { - prettierImportAttempted = true; - - try { - // First try require() which is more stable in npx environments - prettierModule = require("prettier"); - } catch { - try { - // Fallback to dynamic import with error boundary - const importWithTimeout = () => { - return new Promise((resolve, reject) => { - let resolved = false; - - // Set a timeout to prevent infinite hanging - const timeoutId = setTimeout(() => { - if (!resolved) { - resolved = true; - reject(new Error("Prettier import timeout")); - } - }, 3000); - - import("prettier") - .then((module) => { - if (!resolved) { - resolved = true; - clearTimeout(timeoutId); - resolve(module); - } - }) - .catch((error) => { - if (!resolved) { - resolved = true; - clearTimeout(timeoutId); - reject(error); - } - }); - }); - }; - - prettierModule = await importWithTimeout(); - } catch { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn( - warning( - "Failed to load prettier module. Will not use prettier to format output.", - ), - ); - prettierModule = undefined; - } - } - } - return prettierModule; -} - -function safeStringify(obj: unknown): string { - try { - return JSON.stringify(obj, null, 2); - } catch (error) { - // Fallback for circular references or other JSON.stringify issues - try { - return util.inspect(obj, { - depth: 5, - maxStringLength: 1000, - breakLength: 80, - compact: false, - }); - } catch { - return `[Object: Unable to serialize - ${error instanceof Error ? error.message : error}]`; - } - } -} diff --git a/js/src/cli/util/types.ts b/js/src/cli/util/types.ts deleted file mode 100644 index 2909a607e..000000000 --- a/js/src/cli/util/types.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { type IfExistsType as IfExists } from "../../generated_types"; - -export interface CommonArgs { - /** @deprecated Use debug_logging: "debug" for detailed troubleshooting output. */ - verbose: boolean; - debug_logging?: "error" | "warn" | "info" | "debug"; -} - -export interface AuthArgs { - api_key?: string; - org_name?: string; - app_url?: string; - env_file?: string; -} - -export interface CompileArgs { - tsconfig?: string; - terminate_on_failure: boolean; - external_packages?: string[]; -} - -export interface RunArgs extends CommonArgs, AuthArgs, CompileArgs { - files: string[]; - watch: boolean; - list: boolean; - jsonl: boolean; - filter?: string[]; - no_send_logs: boolean; - no_progress_bars: boolean; - bundle: boolean; - push: boolean; - dev: boolean; - dev_host: string; - dev_port: number; - dev_org_name?: string; -} - -export interface BundleArgs extends CommonArgs, AuthArgs, CompileArgs { - files: string[]; - if_exists: IfExists; -} - -export interface PullArgs extends CommonArgs, AuthArgs { - output_dir: string; - project_name?: string; - project_id?: string; - id?: string; - slug?: string; - version?: string; - force: boolean; -} diff --git a/js/src/durable-eval.ts b/js/src/durable-eval.ts index 7a0c45e70..c27f63d26 100644 --- a/js/src/durable-eval.ts +++ b/js/src/durable-eval.ts @@ -852,7 +852,6 @@ async function startDurableEval< stop: () => undefined, increment: () => undefined, }, - [], undefined, parameters, true, diff --git a/js/src/exports.ts b/js/src/exports.ts index e8bac9cc2..d39c15459 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -253,7 +253,6 @@ export type { EvalScorer, EvalClassifier, EvaluatorDef, - EvaluatorFile, ReporterBody, SpanContext, } from "./framework"; @@ -263,9 +262,6 @@ export { Eval, EvalResultWithSummary, Reporter, - buildLocalSummary, - reportFailures, - runEvaluator, defaultErrorScoreHandler, } from "./framework"; @@ -286,7 +282,6 @@ export { DatasetPipeline } from "./dataset-pipeline"; export type { CodeOpts, CreateProjectOpts, - FunctionEvent, PromptOpts, ScorerOpts, } from "./framework2"; @@ -295,12 +290,10 @@ export { CodeFunction, CodePrompt, Project, - ProjectNameIdMap, PromptBuilder, ScorerBuilder, ToolBuilder, projects, - toolFunctionDefinitionSchema, } from "./framework2"; export { @@ -334,21 +327,8 @@ export { addAzureBlobHeaders, LazyValue } from "./util"; export { AttachmentReference } from "./generated_types"; -export type { - EvaluatorManifest, - EvalParameterSerializedSchema, - EvaluatorDefinition, - EvaluatorDefinitions, - ParametersSource, -} from "../dev/types"; - export type { EvalParameters } from "./eval-parameters"; -export { - evaluatorDefinitionSchema, - evaluatorDefinitionsSchema, -} from "../dev/types"; - // Auto-instrumentation configuration export { configureInstrumentation } from "./instrumentation"; export { braintrustFlueInstrumentation } from "./instrumentation"; diff --git a/js/src/framework.test.ts b/js/src/framework.test.ts index 4c86a8235..cb905aad7 100644 --- a/js/src/framework.test.ts +++ b/js/src/framework.test.ts @@ -54,7 +54,6 @@ test("meta (write) is passed to task", async () => { scores: [], }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -92,7 +91,6 @@ test("metadata (read/write) is passed to task", async () => { scores: [], }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -132,7 +130,6 @@ test("expected (read/write) is passed to task", async () => { scores: [], }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -175,7 +172,6 @@ test("EvalCase id and tags are passed to scorers", async () => { ], }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -259,7 +255,6 @@ describe("runEvaluator", () => { scores: [], }, new NoopProgressReporter(), - [], undefined, ); @@ -292,7 +287,6 @@ describe("runEvaluator", () => { scores: [], }, new NoopProgressReporter(), - [], undefined, ); @@ -331,7 +325,6 @@ describe("runEvaluator", () => { scores: [], }, new NoopProgressReporter(), - [], undefined, ); @@ -369,7 +362,6 @@ describe("runEvaluator", () => { scores: [], }, new NoopProgressReporter(), - [], (event) => streamEvents.push(event), ); @@ -408,7 +400,6 @@ describe("runEvaluator", () => { scores: [], }, new NoopProgressReporter(), - [], undefined, ); @@ -432,7 +423,6 @@ describe("runEvaluator", () => { ), }, new NoopProgressReporter(), - [], undefined, ); @@ -459,7 +449,6 @@ describe("runEvaluator", () => { errorScoreHandler: defaultErrorScoreHandler, }, new NoopProgressReporter(), - [], undefined, ); @@ -488,7 +477,6 @@ describe("runEvaluator", () => { errorScoreHandler: defaultErrorScoreHandler, }, new NoopProgressReporter(), - [], undefined, ); @@ -521,7 +509,6 @@ describe("runEvaluator", () => { errorScoreHandler: () => undefined, }, new NoopProgressReporter(), - [], undefined, ); @@ -546,7 +533,6 @@ describe("runEvaluator", () => { errorScoreHandler: () => ({ error_score: 1 }), }, new NoopProgressReporter(), - [], undefined, ); @@ -579,7 +565,6 @@ describe("runEvaluator", () => { }, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -624,7 +609,6 @@ describe("runEvaluator", () => { maxConcurrency: 1, }, new NoopProgressReporter(), - [], undefined, ), ).rejects.toThrow(new InternalAbortError("Evaluator timed out")); @@ -673,7 +657,6 @@ describe("runEvaluator", () => { maxConcurrency: 1, }, new NoopProgressReporter(), - [], undefined, ), ).rejects.toThrow(new InternalAbortError("Evaluator aborted")); @@ -708,7 +691,6 @@ describe("runEvaluator", () => { scores: [], }, new NoopProgressReporter(), - [], undefined, ); @@ -740,7 +722,6 @@ describe("runEvaluator", () => { signal: abortController.signal, }, new NoopProgressReporter(), - [], undefined, ); @@ -769,7 +750,6 @@ test("trialIndex is passed to task", async () => { trialCount: 3, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -811,7 +791,6 @@ test("trialIndex with multiple inputs", async () => { trialCount: 2, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -857,7 +836,6 @@ test("per-input trialCount overrides global trialCount", async () => { trialCount: 2, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -904,7 +882,6 @@ test("per-input trialCount works without global trialCount", async () => { scores: [], }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -1071,7 +1048,6 @@ test("runEvaluator forwards baseExperimentId to summary", async () => { baseExperimentId: "base-exp-id", }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -1114,7 +1090,6 @@ test("runEvaluator forwards persisted baseExperimentName id to summary", async ( baseExperimentName: "base-exp", }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -1156,7 +1131,6 @@ test("tags can be appended and logged to root span", async () => { summarizeScores: false, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -1205,7 +1179,6 @@ test.each([ summarizeScores: false, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -1245,7 +1218,6 @@ test("tags are persisted with a failing scorer", async () => { summarizeScores: false, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -1278,7 +1250,6 @@ test("tags remain empty when not set", async () => { summarizeScores: false, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -1313,7 +1284,6 @@ test("scorer spans have purpose='scorer' attribute", async () => { ], }, new NoopProgressReporter(), - [], undefined, undefined, true, diff --git a/js/src/framework.ts b/js/src/framework.ts index 42d4e09ff..9a463533b 100644 --- a/js/src/framework.ts +++ b/js/src/framework.ts @@ -121,7 +121,7 @@ export type EvalTask< hooks: EvalHooks, ) => Output); -export type TaskProgressEvent = Omit< +type TaskProgressEvent = Omit< SSEProgressEventData, "id" | "origin" | "object_type" | "name" >; @@ -404,7 +404,7 @@ export class EvalResultWithSummary< } } -export type { ReporterBody, ReporterDef } from "./reporters/types"; +export type { ReporterBody } from "./reporters/types"; async function getPersistedBaseExperimentId( experiment: Experiment, @@ -435,7 +435,7 @@ export type EvaluatorDef< evalName: string; } & Evaluator; -export type EvaluatorFile = { +type EvaluatorFile = { functions: CodeFunction< unknown, unknown, @@ -800,7 +800,7 @@ export async function Eval< if (typeof options.reporter === "string") { throw new Error( - "Must specify a reporter object, not a name. Can only specify reporter names when running 'braintrust eval'", + "Must specify a reporter object, not a name. Can only specify reporter names when running 'bt eval'", ); } @@ -847,7 +847,6 @@ export async function Eval< null, evalDef, progressReporter, - [], options.stream, options.parameters, shouldCollectResults, @@ -860,7 +859,6 @@ export async function Eval< experiment, evalDef, progressReporter, - [], options.stream, options.parameters, shouldCollectResults, @@ -903,64 +901,7 @@ export function Reporter( return ret; } -export interface Filter { - path: string[]; - pattern: RegExp; -} - -function serializeJSONWithPlainString(v: unknown) { - if (typeof v === "string") { - return v; - } else { - return JSON.stringify(v); - } -} - -function deserializePlainStringAsJSON(s: string) { - try { - return { value: JSON.parse(s), error: undefined }; - } catch (e) { - return { value: s, error: e }; - } -} - -export function parseFilters(filters: string[]): Filter[] { - const result: Filter[] = []; - for (const f of filters) { - const equalsIdx = f.indexOf("="); - if (equalsIdx === -1) { - throw new Error(`Invalid filter ${f}`); - } - const [path, value] = [f.slice(0, equalsIdx), f.slice(equalsIdx + 1)]; - let deserializedValue = deserializePlainStringAsJSON(value).value; - if (typeof deserializedValue !== "string") { - deserializedValue = value; // Just fall back to the original input - } - result.push({ - path: path.split("."), - pattern: new RegExp(deserializedValue), - }); - } - return result; -} - -function evaluateFilter(object: unknown, filter: Filter) { - const { path, pattern } = filter; - const key = path.reduce( - (acc, p) => - typeof acc === "object" && acc !== null - ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - (acc as Record)[p] - : undefined, - object, - ); - if (key === undefined) { - return false; - } - return pattern.test(serializeJSONWithPlainString(key)); -} - -export function scorerName( +function scorerName( // eslint-disable-next-line @typescript-eslint/no-explicit-any scorer: EvalScorer, scorer_idx: number, @@ -1207,7 +1148,6 @@ export async function runEvaluator( // eslint-disable-next-line @typescript-eslint/no-explicit-any evaluator: EvaluatorDef, progressReporter: ProgressReporter, - filters: Filter[], stream: ((data: SSEProgressEventData) => void) | undefined, parameters?: InferParameters, collectResults = true, @@ -1223,7 +1163,6 @@ export async function runEvaluator( experiment, evaluator, progressReporter, - filters, stream, parameters, collectResults, @@ -1246,7 +1185,6 @@ async function runEvaluatorInternal( // eslint-disable-next-line @typescript-eslint/no-explicit-any evaluator: EvaluatorDef, progressReporter: ProgressReporter, - filters: Filter[], stream: ((data: SSEProgressEventData) => void) | undefined, parameters: InferParameters | undefined, collectResults: boolean, @@ -1616,9 +1554,6 @@ async function runEvaluatorInternal( if (cancelled) { break; } - if (!filters.every((f) => evaluateFilter(datum, f))) { - continue; - } const trialCount = datum.trialCount ?? evaluator.trialCount ?? 1; for (let trialIndex = 0; trialIndex < trialCount; trialIndex++) { if (cancelled) { @@ -1756,10 +1691,9 @@ async function runEvaluatorInternal( } } -export const error = (text: string) => `Error: ${text}`; -export const warning = (text: string) => `Warning: ${text}`; +const warning = (text: string) => `Warning: ${text}`; -export function logError(e: unknown, verbose: boolean) { +function logError(e: unknown, verbose: boolean) { if (!verbose) { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.error(`${e}`); @@ -1829,12 +1763,7 @@ export function buildLocalSummary( }; } -export function reportFailures< - Input, - Output, - Expected, - Metadata extends BaseMetadata, ->( +function reportFailures( evaluator: EvaluatorDef, failingResults: EvalResult[], { verbose, jsonl }: ReporterOpts, diff --git a/js/src/framework2.ts b/js/src/framework2.ts index 47994f5f1..f9220b6e1 100644 --- a/js/src/framework2.ts +++ b/js/src/framework2.ts @@ -9,7 +9,6 @@ import { type SavedFunctionIdType as SavedFunctionId, type PromptBlockDataType as PromptBlockData, type PromptDataType as PromptData, - ToolFunctionDefinition as toolFunctionDefinitionSchema, type ToolFunctionDefinitionType as ToolFunctionDefinition, FunctionData as functionDataSchema, Project as projectSchema, @@ -21,7 +20,6 @@ import { login, Prompt, PromptRowWithId, - RemoteEvalParameters, } from "./logger"; import type { BaseFnOpts, GenericFunction } from "./framework-types"; import type { EvalParameters } from "./eval-parameters"; @@ -30,14 +28,12 @@ import { type PromptDefinition, } from "./prompt-schemas"; import { zodToJsonSchema } from "./zod/utils"; -import type { - ParametersSchema, - StaticParametersSchema, - SerializedParametersContainer, -} from "../dev/types"; - -export { toolFunctionDefinitionSchema }; -// ToolFunctionDefinition exported as type-only from main index to avoid namespace issues +type ParametersSchema = { + type: "object"; + properties: Record>; + required?: string[]; + additionalProperties?: boolean; +}; // Safe access to __filename (only exists in Node.js CJS) const currentFilename = @@ -120,7 +116,7 @@ export class Project { async publish() { if (globalThis._lazy_load) { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("publish() is a no-op when running `braintrust push`."); + console.warn("publish() is a no-op when running `bt push`."); return; } await login(); @@ -129,7 +125,7 @@ export class Project { if (this._publishableCodeFunctions.length > 0) { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.warn( - "Code functions cannot be published directly. Use `braintrust push` instead.", + "Code functions cannot be published directly. Use `bt push` instead.", ); } if (this._publishablePrompts.length > 0) { @@ -710,53 +706,6 @@ class ParametersBuilder { } } -export function serializeEvalParametersToStaticParametersSchema( - parameters: EvalParameters, -): StaticParametersSchema { - return Object.fromEntries( - Object.entries(parameters).map(([name, value]) => { - if ("type" in value && value.type === "prompt") { - return [ - name, - { - type: "prompt", - default: value.default - ? promptDefinitionToPromptData(value.default) - : undefined, - description: value.description, - }, - ]; - } else if ("type" in value && value.type === "model") { - return [ - name, - { - type: "model", - default: value.default, - description: value.description, - }, - ]; - } else { - // Since this schema is bundled, it won't pass an instanceof check. For - // some reason, aliasing it to `z.ZodSchema` leads to `error TS2589: - // Type instantiation is excessively deep and possibly infinite.` So - // just using `any` to turn off the typesystem. - // - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const schemaObj = zodToJsonSchema(value as unknown as z.ZodType); - return [ - name, - { - type: "data", - schema: schemaObj, - default: schemaObj.default, - description: schemaObj.description, - }, - ]; - } - }), - ); -} - function serializeEvalParameterstoParametersSchema( parameters: EvalParameters, ): ParametersSchema { @@ -827,32 +776,7 @@ function getDefaultDataFromParametersSchema( ); } -export function serializeRemoteEvalParametersContainer( - parameters: EvalParameters | RemoteEvalParameters, -): SerializedParametersContainer { - if (RemoteEvalParameters.isParameters(parameters)) { - return { - type: "braintrust.parameters", - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - schema: parameters.schema as ParametersSchema, - source: { - parametersId: parameters.id, - slug: parameters.slug, - name: parameters.name, - projectId: parameters.projectId, - version: parameters.version, - }, - }; - } - - return { - type: "braintrust.staticParameters", - schema: serializeEvalParametersToStaticParametersSchema(parameters), - source: null, - }; -} - -export interface FunctionEvent { +interface FunctionEvent { project_id: string; slug: string; name: string; @@ -866,7 +790,7 @@ export interface FunctionEvent { environments?: { slug: string }[]; } -export class ProjectNameIdMap { +class ProjectNameIdMap { private nameToId: Record = {}; private idToName: Record = {}; diff --git a/js/src/gitutil.ts b/js/src/gitutil.ts index 7b23999af..8e318ac98 100644 --- a/js/src/gitutil.ts +++ b/js/src/gitutil.ts @@ -171,7 +171,7 @@ export async function getRepoInfo(settings?: GitMetadataSettings) { return sanitized; } -export async function currentRepoPath(): Promise { +async function currentRepoPath(): Promise { return await attempt(async () => (await runGitCommand(["rev-parse", "--show-toplevel"])).trim(), ); diff --git a/js/src/imports.test.ts b/js/src/imports.test.ts index 96a4851d5..c2d53c767 100644 --- a/js/src/imports.test.ts +++ b/js/src/imports.test.ts @@ -2,88 +2,7 @@ import { describe, it, expect } from "vitest"; import * as fs from "fs"; import * as path from "path"; -describe("CLI import restrictions", () => { - it("should not import from cli directory in non-cli code", () => { - const srcDir = path.join(__dirname); - const violations: string[] = []; - - function walkDirectory(dir: string) { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - const relativePath = path.relative(srcDir, fullPath); - - // Skip CLI directory and test files - if (relativePath.startsWith("cli/") || relativePath === "cli") { - continue; - } - - if (entry.isDirectory() && entry.name === "node_modules") { - continue; - } - - if (entry.isDirectory()) { - walkDirectory(fullPath); - } else if ( - entry.isFile() && - (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) && - !entry.name.endsWith(".d.ts") && - !entry.name.endsWith(".d.tsx") - ) { - checkFileForCliImports(fullPath, relativePath); - } - } - } - - function checkFileForCliImports(filePath: string, relativePath: string) { - const content = fs.readFileSync(filePath, "utf-8"); - const lines = content.split("\n"); - - lines.forEach((line, index) => { - // Check for import statements that reference CLI - const importMatch = - line.match(/from\s+["']([^"']+)["']/) || - line.match(/import\s*\(\s*["']([^"']+)["']\s*\)/); - - if (importMatch) { - const importPath = importMatch[1]; - - // Check if the import path references the CLI directory - // Only match actual /cli/ directory paths, not package names containing "cli" - // (e.g., @temporalio/client should not match) - if ( - importPath.includes("/cli/") || - importPath === "./cli" || - importPath === "../cli" || - importPath.endsWith("/cli") || - importPath.match(/^\.\.\/.*\/cli$/) || - importPath.match(/^\.\.\/.*\/cli\//) - ) { - violations.push( - `${relativePath}:${index + 1} - Illegal import from CLI: "${importPath}"`, - ); - } - } - }); - } - - walkDirectory(srcDir); - - if (violations.length > 0) { - const message = [ - "Found illegal imports from CLI directory in SDK code:", - "", - ...violations, - "", - "SDK code (src/**) must not import from CLI code (src/cli/**).", - "CLI code can import from SDK code, but not vice versa.", - ].join("\n"); - - expect.fail(message); - } - }); - +describe("Import restrictions", () => { it("should not allow eslint-disable comments for no-restricted-imports", () => { const srcDir = path.join(__dirname); const violations: string[] = []; @@ -95,11 +14,6 @@ describe("CLI import restrictions", () => { const fullPath = path.join(dir, entry.name); const relativePath = path.relative(srcDir, fullPath); - // Skip CLI directory and test files - if (relativePath.startsWith("cli/") || relativePath === "cli") { - continue; - } - if (entry.isDirectory() && entry.name === "node_modules") { continue; } @@ -153,7 +67,7 @@ describe("CLI import restrictions", () => { ...violations, "", "Disabling the no-restricted-imports rule is not allowed.", - "This rule prevents SDK code from importing CLI code and cannot be bypassed.", + "This rule protects SDK module boundaries and cannot be bypassed.", "If you believe you have a legitimate need for this import, please discuss with the team.", ].join("\n"); @@ -172,11 +86,6 @@ describe("CLI import restrictions", () => { const fullPath = path.join(dir, entry.name); const relativePath = path.relative(srcDir, fullPath); - // Skip the cli directory - CLI code is allowed to use require() and dynamic imports - if (entry.isDirectory() && entry.name === "cli") { - continue; - } - // Skip node_modules directories (test fixture deps, not SDK source) if (entry.isDirectory() && entry.name === "node_modules") { continue; diff --git a/js/src/node/index.ts b/js/src/node/index.ts index ba75fa586..60e1efe09 100644 --- a/js/src/node/index.ts +++ b/js/src/node/index.ts @@ -42,10 +42,10 @@ * }); * ``` * - * Finally, run the script with `npx braintrust eval hello.eval.ts`. + * Finally, run the script with `npx bt eval hello.eval.ts`. * * ```bash - * BRAINTRUST_API_KEY= npx braintrust eval hello.eval.ts + * BRAINTRUST_API_KEY= npx bt eval hello.eval.ts * ``` * * @module braintrust diff --git a/js/src/parameters.test.ts b/js/src/parameters.test.ts index b22953561..69576dd9a 100644 --- a/js/src/parameters.test.ts +++ b/js/src/parameters.test.ts @@ -35,7 +35,6 @@ test("parameters are passed to task", async () => { }, }, new NoopProgressReporter(), - [], undefined, { prefix: "start:", suffix: ":end" }, true, @@ -81,7 +80,6 @@ test("prompt parameter is passed correctly", async () => { }, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -163,7 +161,6 @@ test("custom parameter values override defaults", async () => { }, }, new NoopProgressReporter(), - [], undefined, { prefix: "custom:", @@ -195,7 +192,6 @@ test("array parameter is handled correctly", async () => { }, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -234,7 +230,6 @@ test("object parameter is handled correctly", async () => { }, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -265,7 +260,6 @@ test("model parameter defaults to configured value", async () => { }, }, new NoopProgressReporter(), - [], undefined, undefined, true, @@ -293,7 +287,6 @@ test("model parameter is required when default is missing", async () => { }, }, new NoopProgressReporter(), - [], undefined, undefined, true, diff --git a/js/src/reporters/types.ts b/js/src/reporters/types.ts index f68724bf6..a85be9bdb 100644 --- a/js/src/reporters/types.ts +++ b/js/src/reporters/types.ts @@ -32,7 +32,7 @@ export interface ReporterBody { /** * A function that takes all evaluator results and returns a boolean indicating - * whether the run was successful. If you return false, the `braintrust eval` + * whether the run was successful. If you return false, the `bt eval` * command will exit with a non-zero status code. * * @param reports diff --git a/js/src/zod/zod-serialization-test-shared.ts b/js/src/zod/zod-serialization-test-shared.ts deleted file mode 100644 index a78f4b708..000000000 --- a/js/src/zod/zod-serialization-test-shared.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Shared test expectations for zod serialization tests - * - * This module contains expected JSON Schema outputs used by both v3 and v4 - * serialization tests to ensure consistent behavior across versions. - */ - -/** - * Expected JSON schema for string parameter with description and default - */ - -export const EXPECTED_STRING_SCHEMA = { - type: "string", - description: "The instructions for the agent", - default: "You are a helpful assistant.", - $schema: "http://json-schema.org/draft-07/schema#", -}; - -/** - * Expected JSON schema for number parameter with constraints - */ - -export const EXPECTED_NUMBER_SCHEMA = { - type: "number", - minimum: 0, - maximum: 2, - description: "Temperature for LLM", - default: 0.7, - $schema: "http://json-schema.org/draft-07/schema#", -}; - -/** - * Expected JSON schema for object parameter - */ - -export const EXPECTED_OBJECT_SCHEMA = { - type: "object", - properties: { - model: { type: "string" }, - maxTokens: { type: "number" }, - }, - additionalProperties: false, - required: ["model"], - description: "Configuration object", - $schema: "http://json-schema.org/draft-07/schema#", -}; - -/** - * Expected JSON schema for enum parameter - */ - -export const EXPECTED_ENUM_SCHEMA = { - type: "string", - enum: ["fast", "accurate", "balanced"], - description: "Processing mode", - default: "balanced", - $schema: "http://json-schema.org/draft-07/schema#", -}; - -/** - * Expected JSON schema for array parameter - */ - -export const EXPECTED_ARRAY_SCHEMA = { - type: "array", - items: { type: "string" }, - description: "Tags for filtering", - default: ["default"], - $schema: "http://json-schema.org/draft-07/schema#", -}; diff --git a/js/src/zod/zod-v3-serialization.test.ts b/js/src/zod/zod-v3-serialization.test.ts deleted file mode 100644 index c4d1097d2..000000000 --- a/js/src/zod/zod-v3-serialization.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Tests for zod v3 compatibility with serializeEvalParametersToStaticParametersSchema - * - * This file tests serializeEvalParametersToStaticParametersSchema specifically with zod v3. - * See zod-v4-serialization.test.ts for zod v4 specific tests. - */ - -import { test, describe, beforeEach, expect } from "vitest"; -import * as zodModule from "zod"; -import { z } from "zod"; -import { - EXPECTED_STRING_SCHEMA, - EXPECTED_NUMBER_SCHEMA, - EXPECTED_OBJECT_SCHEMA, - EXPECTED_ENUM_SCHEMA, - EXPECTED_ARRAY_SCHEMA, -} from "./zod-serialization-test-shared"; -import { - serializeEvalParametersToStaticParametersSchema, - serializeRemoteEvalParametersContainer, -} from "../framework2"; -import { - serializedParametersContainerSchema, - evaluatorDefinitionSchema, -} from "../../dev/types"; -// Detect which zod version is installed by checking for v4-specific properties -function getInstalledZodVersion(): 3 | 4 { - const testSchema = zodModule.z.string(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return "_zod" in (testSchema as any) ? 4 : 3; -} - -describe("serializeEvalParametersToStaticParametersSchema with Zod v3", () => { - beforeEach(() => { - const version = getInstalledZodVersion(); - expect(version).toBe(3); - }); - - test("string schema serializes correctly", () => { - const parameters = { - instructions: z - .string() - .describe("The instructions for the agent") - .default("You are a helpful assistant."), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - console.log(result); - expect(result.instructions).toBeDefined(); - expect(result.instructions.type).toBe("data"); - expect( - result.instructions.type === "data" && result.instructions.schema, - ).toStrictEqual(EXPECTED_STRING_SCHEMA); - expect(result.instructions.description).toBe( - "The instructions for the agent", - ); - expect(result.instructions.default).toBe("You are a helpful assistant."); - }); - - test("number schema serializes correctly", () => { - const parameters = { - temperature: z - .number() - .min(0) - .max(2) - .describe("Temperature for LLM") - .default(0.7), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - result.temperature.type; - - expect(result.temperature.type).toBe("data"); - expect( - result.temperature.type === "data" && result.temperature.schema, - ).toStrictEqual(EXPECTED_NUMBER_SCHEMA); - expect(result.temperature.description).toBe("Temperature for LLM"); - expect(result.temperature.default).toBe(0.7); - }); - - test("object schema serializes correctly", () => { - const parameters = { - config: z - .object({ - model: z.string(), - maxTokens: z.number().optional(), - }) - .describe("Configuration object"), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.config.type).toBe("data"); - expect(result.config.type === "data" && result.config.schema).toStrictEqual( - EXPECTED_OBJECT_SCHEMA, - ); - expect(result.config.description).toBe("Configuration object"); - }); - - test("enum schema serializes correctly", () => { - const parameters = { - mode: z - .enum(["fast", "accurate", "balanced"]) - .describe("Processing mode") - .default("balanced"), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.mode.type).toBe("data"); - expect(result.mode.type === "data" && result.mode.schema).toStrictEqual( - EXPECTED_ENUM_SCHEMA, - ); - expect(result.mode.description).toBe("Processing mode"); - expect(result.mode.default).toBe("balanced"); - }); - - test("array schema serializes correctly", () => { - const parameters = { - tags: z - .array(z.string()) - .describe("Tags for filtering") - .default(["default"]), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.tags.type).toBe("data"); - expect(result.tags.type === "data" && result.tags.schema).toStrictEqual( - EXPECTED_ARRAY_SCHEMA, - ); - expect(result.tags.description).toBe("Tags for filtering"); - expect(result.tags.default).toEqual(["default"]); - }); - - test("model parameter serializes correctly", () => { - const parameters = { - model: { - type: "model" as const, - description: "Model to use", - default: "gpt-5-mini", - }, - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.model.type).toBe("model"); - expect(result.model.description).toBe("Model to use"); - expect(result.model.default).toBe("gpt-5-mini"); - }); -}); - -describe("serializeRemoteEvalParametersContainer with Zod v3", () => { - beforeEach(() => { - const version = getInstalledZodVersion(); - expect(version).toBe(3); - }); - - test("local parameters are serialized with legacyParameters type", () => { - const parameters = { - instructions: z - .string() - .describe("The instructions for the agent") - .default("You are a helpful assistant."), - }; - - const result = serializeRemoteEvalParametersContainer(parameters); - - expect(result.type).toBe("braintrust.staticParameters"); - expect(result.source).toBeNull(); - if (result.type === "braintrust.staticParameters") { - expect(result.schema.instructions).toBeDefined(); - expect(result.schema.instructions.type).toBe("data"); - } - }); -}); - -describe("serializedParametersContainerSchema parsing", () => { - const schema = { - model: { - type: "model" as const, - description: "Model to use", - default: "gpt-5-mini", - }, - }; - - test("parses braintrust.staticParameters with source: null", () => { - const result = serializedParametersContainerSchema.safeParse({ - type: "braintrust.staticParameters", - schema, - source: null, - }); - expect(result.success).toBe(true); - }); - - test("parses braintrust.staticParameters with source omitted", () => { - const result = serializedParametersContainerSchema.safeParse({ - type: "braintrust.staticParameters", - schema, - }); - expect(result.success).toBe(true); - }); -}); - -describe("evaluatorDefinitionSchema parameters nullish", () => { - test("allows parameters: null", () => { - const result = evaluatorDefinitionSchema.safeParse({ - parameters: null, - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.parameters).toBeNull(); - } - }); - - test("allows parameters: undefined", () => { - const result = evaluatorDefinitionSchema.safeParse({ - parameters: undefined, - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.parameters).toBeUndefined(); - } - }); - - test("allows parameters omitted entirely", () => { - const result = evaluatorDefinitionSchema.safeParse({}); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.parameters).toBeUndefined(); - } - }); -}); diff --git a/js/src/zod/zod-v4-serialization.test.ts b/js/src/zod/zod-v4-serialization.test.ts deleted file mode 100644 index 9ba907e01..000000000 --- a/js/src/zod/zod-v4-serialization.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Tests for zod v4 compatibility with serializeEvalParametersToStaticParametersSchema - * - * This file tests makeEvalParametersSchema specifically with zod v4. - * See zod-v3-serialization.test.ts for zod v3 specific tests. - */ - -import { test, describe, beforeEach, expect } from "vitest"; -import * as zodModule from "zod"; -import { z } from "zod"; -import { - EXPECTED_STRING_SCHEMA, - EXPECTED_NUMBER_SCHEMA, - EXPECTED_OBJECT_SCHEMA, - EXPECTED_ENUM_SCHEMA, - EXPECTED_ARRAY_SCHEMA, -} from "./zod-serialization-test-shared"; -import { serializeEvalParametersToStaticParametersSchema } from "../framework2"; -// Detect which zod version is installed by checking for v4-specific properties -function getInstalledZodVersion(): 3 | 4 { - const testSchema = zodModule.z.string(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return "_zod" in (testSchema as any) ? 4 : 3; -} -describe("makeEvalParametersSchema with Zod v4", () => { - beforeEach(() => { - const version = getInstalledZodVersion(); - expect(version).toBe(4); - }); - - test("string schema serializes correctly", () => { - const parameters = { - instructions: z - .string() - .describe("The instructions for the agent") - .default("You are a helpful assistant."), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - expect(result.instructions).toBeDefined(); - expect(result.instructions.type).toBe("data"); - expect( - result.instructions.type === "data" && result.instructions.schema, - ).toStrictEqual(EXPECTED_STRING_SCHEMA); - expect(result.instructions.description).toBe( - "The instructions for the agent", - ); - expect(result.instructions.default).toBe("You are a helpful assistant."); - }); - - test("number schema serializes correctly", () => { - const parameters = { - temperature: z - .number() - .min(0) - .max(2) - .describe("Temperature for LLM") - .default(0.7), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.temperature.type).toBe("data"); - expect( - result.temperature.type === "data" && result.temperature.schema, - ).toStrictEqual(EXPECTED_NUMBER_SCHEMA); - expect(result.temperature.description).toBe("Temperature for LLM"); - expect(result.temperature.default).toBe(0.7); - }); - - test("object schema serializes correctly", () => { - const parameters = { - config: z - .object({ - model: z.string(), - maxTokens: z.number().optional(), - }) - .describe("Configuration object"), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.config.type).toBe("data"); - expect(result.config.type === "data" && result.config.schema).toStrictEqual( - EXPECTED_OBJECT_SCHEMA, - ); - expect(result.config.description).toBe("Configuration object"); - }); - - test("enum schema serializes correctly", () => { - const parameters = { - mode: z - .enum(["fast", "accurate", "balanced"]) - .describe("Processing mode") - .default("balanced"), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.mode.type).toBe("data"); - expect(result.mode.type === "data" && result.mode.schema).toStrictEqual( - EXPECTED_ENUM_SCHEMA, - ); - expect(result.mode.description).toBe("Processing mode"); - expect(result.mode.default).toBe("balanced"); - }); - - test("array schema serializes correctly", () => { - const parameters = { - tags: z - .array(z.string()) - .describe("Tags for filtering") - .default(["default"]), - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.tags.type).toBe("data"); - expect(result.tags.type === "data" && result.tags.schema).toStrictEqual( - EXPECTED_ARRAY_SCHEMA, - ); - expect(result.tags.description).toBe("Tags for filtering"); - expect(result.tags.default).toEqual(["default"]); - }); - - test("model parameter serializes correctly", () => { - const parameters = { - model: { - type: "model" as const, - description: "Model to use", - default: "gpt-5-mini", - }, - }; - - const result = serializeEvalParametersToStaticParametersSchema(parameters); - - expect(result.model.type).toBe("model"); - expect(result.model.description).toBe("Model to use"); - expect(result.model.default).toBe("gpt-5-mini"); - }); -}); diff --git a/js/tests/api-compatibility/api-compatibility.test.ts b/js/tests/api-compatibility/api-compatibility.test.ts index 99768dd48..f11e3d847 100644 --- a/js/tests/api-compatibility/api-compatibility.test.ts +++ b/js/tests/api-compatibility/api-compatibility.test.ts @@ -20,7 +20,7 @@ import type { Options } from "tsup"; * ## How It Works * * 1. Downloads the latest published version from npm - * 2. Extracts the .d.ts files for each entrypoint (main, browser, dev, util) + * 2. Extracts the .d.ts files for each entrypoint (main, browser, util) * 3. Parses both published and current .d.ts files using TypeScript Compiler API * 4. Compares exported symbols (functions, classes, interfaces, types, etc.) * 5. Fails if breaking changes are detected in non-major version bumps @@ -100,7 +100,6 @@ function getEntrypointName(entryFile: string, outDir: string): string { if (entryFile.includes("src/browser/index.ts")) return "browser"; if (entryFile.includes("src/edge-light/index.ts")) return "edge-light"; if (entryFile.includes("src/workerd/index.ts")) return "workerd"; - if (entryFile.includes("dev/index.ts")) return "dev"; if (entryFile.includes("util/index.ts")) return "util"; // Default to basename @@ -152,6 +151,45 @@ function getVersionBumpType( return "none"; } +function getChangesetBumpType( + packageName: string, +): "major" | "minor" | "patch" | null { + const changesetDir = path.join(__dirname, "..", "..", "..", ".changeset"); + if (!fs.existsSync(changesetDir)) { + return null; + } + + const bumpPriority = { patch: 1, minor: 2, major: 3 } as const; + let highestBump: "major" | "minor" | "patch" | null = null; + const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const packageBumpPattern = new RegExp( + `^\\s*["']?${escapedPackageName}["']?\\s*:\\s*(major|minor|patch)\\s*$`, + "m", + ); + + for (const file of fs.readdirSync(changesetDir)) { + if (!file.endsWith(".md")) { + continue; + } + + const contents = fs.readFileSync(path.join(changesetDir, file), "utf8"); + const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(contents)?.[1]; + const bump = frontmatter?.match(packageBumpPattern)?.[1] as + | "major" + | "minor" + | "patch" + | undefined; + if ( + bump && + (!highestBump || bumpPriority[bump] > bumpPriority[highestBump]) + ) { + highestBump = bump; + } + } + + return highestBump; +} + /** * Extracts exports and their signatures from a .d.ts file. * For bulk exports (export { A, B, C }), looks up the actual declaration @@ -2857,13 +2895,21 @@ describe("API Compatibility", () => { // Determine version bump type const publishedVersionInfo = parseVersion(publishedVersion); const currentVersionInfo = parseVersion(currentVersion); - versionBumpType = getVersionBumpType( + const packageVersionBumpType = getVersionBumpType( publishedVersionInfo, currentVersionInfo, ); + const changesetBumpType = getChangesetBumpType("braintrust"); + const bumpPriority = { none: 0, patch: 1, minor: 2, major: 3 } as const; + versionBumpType = + changesetBumpType && + bumpPriority[changesetBumpType] > bumpPriority[packageVersionBumpType] + ? changesetBumpType + : packageVersionBumpType; console.log(`Published version: ${publishedVersion}`); console.log(`Current version: ${currentVersion}`); + console.log(`Changeset bump type: ${changesetBumpType ?? "none"}`); console.log(`Version bump type: ${versionBumpType}`); // Create temp directory for downloaded package diff --git a/js/tsup.config.ts b/js/tsup.config.ts index 14bca6b36..416ef9679 100644 --- a/js/tsup.config.ts +++ b/js/tsup.config.ts @@ -32,33 +32,6 @@ export default defineConfig([ define, clean: true, }, - { - entry: { cli: "src/cli/index.ts" }, - format: ["cjs"], - removeNodeProtocol: false, - outDir: "dist", - external: ["esbuild", "prettier", "typescript", "zod"], - // CLI doesn't need DTS - dts: false, - define, - clean: false, - }, - { - entry: ["dev/index.ts"], - format: ["cjs", "esm"], - outDir: "dev/dist", - removeNodeProtocol: false, - external: ["esbuild", "prettier", "typescript", "zod"], - dts: { - // Split DTS generation to reduce memory usage - compilerOptions: { - skipLibCheck: true, - }, - }, - splitting: true, - define, - clean: true, - }, { entry: ["util/index.ts"], format: ["cjs", "esm"], diff --git a/knip.jsonc b/knip.jsonc index 40f840f2d..202457490 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -55,7 +55,6 @@ "scripts/**/*.ts", "examples/**/*.{ts,tsx,js,jsx,mjs,cjs}", "smoke/**/*.{ts,tsx,js,jsx,mjs,cjs}", - "dev/**/*.ts", "util/**/*.ts", ], "project": [ @@ -64,7 +63,6 @@ "scripts/**/*.ts", "examples/**/*.{ts,tsx,js,jsx,mjs,cjs}", "smoke/**/*.{ts,tsx,js,jsx,mjs,cjs,d.ts}", - "dev/**/*.ts", "util/**/*.ts", ], }, diff --git a/package.json b/package.json index 78f725852..aa3b53f88 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,6 @@ "dotagents:sync": "dotagents sync", "changeset": "changeset", "playground": "turbo run playground --filter=\"braintrust\"", - "playground:cli:push": "turbo run playground:cli:push --filter=\"braintrust\"", - "playground:cli:eval": "turbo run playground:cli:eval --filter=\"braintrust\"", "formatting": "prettier --check .", "lint": "turbo run lint", "fix:formatting": "prettier --write .", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79b23cd1e..c6deed80a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -316,9 +316,6 @@ importers: js: dependencies: - '@next/env': - specifier: ^14.2.3 - version: 14.2.3 '@vercel/functions': specifier: ^1.0.2 version: 1.0.2 @@ -331,24 +328,12 @@ importers: ajv: specifier: ^8.20.0 version: 8.20.0 - argparse: - specifier: ^2.0.1 - version: 2.0.1 astring: specifier: ^1.9.0 version: 1.9.0 cjs-module-lexer: specifier: ^2.2.0 version: 2.2.0 - cli-progress: - specifier: ^3.12.0 - version: 3.12.0 - cli-table3: - specifier: ^0.6.5 - version: 0.6.5 - cors: - specifier: ^2.8.5 - version: 2.8.5 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -361,36 +346,21 @@ importers: eventsource-parser: specifier: ^1.1.2 version: 1.1.2 - express: - specifier: ^5.2.1 - version: 5.2.1 - http-errors: - specifier: ^2.0.0 - version: 2.0.1 meriyah: specifier: ^6.1.4 version: 6.1.4 - minimatch: - specifier: ^10.2.5 - version: 10.2.5 module-details-from-path: specifier: ^1.0.4 version: 1.0.4 mustache: specifier: ^4.2.0 version: 4.2.0 - pluralize: - specifier: ^8.0.0 - version: 8.0.0 semifies: specifier: ^1.0.0 version: 1.0.0 source-map: specifier: ^0.7.4 version: 0.7.6 - termi-link: - specifier: ^1.0.1 - version: 1.1.0 unplugin: specifier: ^2.3.5 version: 2.3.11 @@ -404,39 +374,18 @@ importers: '@anthropic-ai/sdk': specifier: ^0.60.0 version: 0.60.0 - '@nodelib/fs.walk': - specifier: ^1.2.8 - version: 1.2.8 - '@types/argparse': - specifier: ^2.0.14 - version: 2.0.14 '@types/async': specifier: ^3.2.24 version: 3.2.24 - '@types/cli-progress': - specifier: ^3.11.5 - version: 3.11.5 - '@types/cors': - specifier: ^2.8.17 - version: 2.8.17 '@types/esquery': specifier: ^1.5.4 version: 1.5.4 - '@types/express': - specifier: ^5.0.0 - version: 5.0.1 - '@types/http-errors': - specifier: ^2.0.4 - version: 2.0.4 '@types/mustache': specifier: ^4.2.5 version: 4.2.5 '@types/node': specifier: ^20.10.5 version: 20.19.40 - '@types/pluralize': - specifier: ^0.0.30 - version: 0.0.30 '@typescript-eslint/eslint-plugin': specifier: ^8.49.0 version: 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4))(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4) @@ -686,10 +635,6 @@ packages: resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} engines: {node: '>= 20.12.0'} - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} @@ -1111,9 +1056,6 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@14.2.3': - resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1779,27 +1721,12 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/argparse@2.0.14': - resolution: {integrity: sha512-jJ6NMs9rXQ0rsqNt3TL4Elcwhd6wygo3lJOVoiHzURD34vsCcAlw443uGu4PXTtEmMF7sYKoadTCLXNmuJuQGw==} - '@types/async@3.2.24': resolution: {integrity: sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==} - '@types/body-parser@1.19.5': - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/cli-progress@3.11.5': - resolution: {integrity: sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/cors@2.8.17': - resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1818,21 +1745,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@5.0.6': - resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} - - '@types/express@5.0.1': - resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} - - '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/mustache@4.2.5': resolution: {integrity: sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==} @@ -1851,24 +1766,9 @@ packages: '@types/nunjucks@3.2.6': resolution: {integrity: sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==} - '@types/pluralize@0.0.30': - resolution: {integrity: sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==} - - '@types/qs@6.9.18': - resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/send@0.17.4': - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - - '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} @@ -2154,10 +2054,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2179,10 +2075,6 @@ packages: brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} - engines: {node: 20 || >=22} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2262,14 +2154,6 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-progress@3.12.0: - resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} - engines: {node: '>=4'} - - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - cli-truncate@5.1.1: resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} engines: {node: '>=20'} @@ -3201,10 +3085,6 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -3506,10 +3386,6 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -3853,10 +3729,6 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - termi-link@1.1.0: - resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} - engines: {node: '>=12'} - terser-webpack-plugin@5.6.0: resolution: {integrity: sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==} engines: {node: '>= 10.13.0'} @@ -4558,9 +4430,6 @@ snapshots: fast-wrap-ansi: 0.2.0 sisteransi: 1.0.5 - '@colors/colors@1.5.0': - optional: true - '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -4986,8 +4855,6 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@14.2.3': {} - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5548,32 +5415,13 @@ snapshots: tslib: 2.8.1 optional: true - '@types/argparse@2.0.14': {} - '@types/async@3.2.24': {} - '@types/body-parser@1.19.5': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 22.19.1 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/cli-progress@3.11.5': - dependencies: - '@types/node': 22.19.1 - - '@types/connect@3.4.38': - dependencies: - '@types/node': 22.19.1 - - '@types/cors@2.8.17': - dependencies: - '@types/node': 22.19.1 - '@types/deep-eql@4.0.2': {} '@types/eslint-scope@3.7.7': @@ -5594,25 +5442,8 @@ snapshots: '@types/estree@1.0.9': {} - '@types/express-serve-static-core@5.0.6': - dependencies: - '@types/node': 22.19.1 - '@types/qs': 6.9.18 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - - '@types/express@5.0.1': - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 5.0.6 - '@types/serve-static': 1.15.7 - - '@types/http-errors@2.0.4': {} - '@types/json-schema@7.0.15': {} - '@types/mime@1.3.5': {} - '@types/mustache@4.2.5': {} '@types/node@12.20.55': {} @@ -5631,25 +5462,8 @@ snapshots: '@types/nunjucks@3.2.6': {} - '@types/pluralize@0.0.30': {} - - '@types/qs@6.9.18': {} - - '@types/range-parser@1.2.7': {} - '@types/retry@0.12.0': {} - '@types/send@0.17.4': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 22.19.1 - - '@types/serve-static@1.15.7': - dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 22.19.1 - '@types/send': 0.17.4 - '@types/set-cookie-parser@2.4.10': dependencies: '@types/node': 22.19.1 @@ -5914,6 +5728,7 @@ snapshots: dependencies: mime-types: 3.0.1 negotiator: 1.0.0 + optional: true acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: @@ -6007,8 +5822,6 @@ snapshots: balanced-match@1.0.2: {} - balanced-match@4.0.4: {} - base64-js@1.5.1: {} baseline-browser-mapping@2.9.14: {} @@ -6030,6 +5843,7 @@ snapshots: type-is: 2.1.0 transitivePeerDependencies: - supports-color + optional: true brace-expansion@1.1.18: dependencies: @@ -6040,10 +5854,6 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.9: - dependencies: - balanced-match: 4.0.4 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -6063,7 +5873,8 @@ snapshots: esbuild: 0.28.1 load-tsconfig: 0.2.5 - bytes@3.1.2: {} + bytes@3.1.2: + optional: true cac@6.7.14: {} @@ -6071,11 +5882,13 @@ snapshots: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 + optional: true call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + optional: true callsites@3.1.0: {} @@ -6108,16 +5921,6 @@ snapshots: dependencies: restore-cursor: 5.1.0 - cli-progress@3.12.0: - dependencies: - string-width: 4.2.3 - - cli-table3@0.6.5: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - cli-truncate@5.1.1: dependencies: slice-ansi: 7.1.2 @@ -6162,16 +5965,21 @@ snapshots: content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 + optional: true - content-type@1.0.5: {} + content-type@1.0.5: + optional: true - content-type@2.0.0: {} + content-type@2.0.0: + optional: true convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} + cookie-signature@1.2.2: + optional: true - cookie@0.7.2: {} + cookie@0.7.2: + optional: true cookie@1.1.1: {} @@ -6179,6 +5987,7 @@ snapshots: dependencies: object-assign: 4.1.1 vary: 1.1.2 + optional: true cross-env@7.0.3: dependencies: @@ -6200,7 +6009,8 @@ snapshots: deep-is@0.1.4: {} - depd@2.0.0: {} + depd@2.0.0: + optional: true detect-indent@6.1.0: {} @@ -6219,10 +6029,12 @@ snapshots: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + optional: true eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} + ee-first@1.1.1: + optional: true electron-to-chromium@1.5.267: {} @@ -6232,7 +6044,8 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} + encodeurl@2.0.0: + optional: true encoding@0.1.13: dependencies: @@ -6251,15 +6064,18 @@ snapshots: environment@1.1.0: {} - es-define-property@1.0.1: {} + es-define-property@1.0.1: + optional: true - es-errors@1.3.0: {} + es-errors@1.3.0: + optional: true es-module-lexer@2.1.0: {} es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 + optional: true esbuild@0.28.1: optionalDependencies: @@ -6292,7 +6108,8 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: {} + escape-html@1.0.3: + optional: true escape-string-regexp@4.0.0: {} @@ -6420,7 +6237,8 @@ snapshots: esutils@2.0.3: {} - etag@1.8.1: {} + etag@1.8.1: + optional: true event-target-shim@5.0.1: {} @@ -6479,6 +6297,7 @@ snapshots: vary: 1.1.2 transitivePeerDependencies: - supports-color + optional: true extendable-error@0.1.7: {} @@ -6538,6 +6357,7 @@ snapshots: statuses: 2.0.2 transitivePeerDependencies: - supports-color + optional: true find-up@4.1.0: dependencies: @@ -6571,9 +6391,11 @@ snapshots: dependencies: fd-package-json: 2.0.0 - forwarded@0.2.0: {} + forwarded@0.2.0: + optional: true - fresh@2.0.0: {} + fresh@2.0.0: + optional: true fs-extra@7.0.1: dependencies: @@ -6594,7 +6416,8 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: {} + function-bind@1.1.2: + optional: true get-caller-file@2.0.5: {} @@ -6612,11 +6435,13 @@ snapshots: has-symbols: 1.1.0 hasown: 2.0.3 math-intrinsics: 1.1.0 + optional: true get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + optional: true get-tsconfig@4.13.0: dependencies: @@ -6669,7 +6494,8 @@ snapshots: globrex@0.1.2: {} - gopd@1.2.0: {} + gopd@1.2.0: + optional: true graceful-fs@4.2.11: {} @@ -6686,11 +6512,13 @@ snapshots: has-flag@4.0.0: {} - has-symbols@1.1.0: {} + has-symbols@1.1.0: + optional: true hasown@2.0.3: dependencies: function-bind: 1.1.2 + optional: true headers-polyfill@5.0.1: dependencies: @@ -6709,6 +6537,7 @@ snapshots: setprototypeof: 1.2.0 statuses: 2.0.2 toidentifier: 1.0.1 + optional: true human-id@4.1.3: {} @@ -6751,7 +6580,8 @@ snapshots: ip-address@10.4.0: optional: true - ipaddr.js@1.9.1: {} + ipaddr.js@1.9.1: + optional: true is-extglob@2.1.1: {} @@ -6769,7 +6599,8 @@ snapshots: is-number@7.0.0: {} - is-promise@4.0.0: {} + is-promise@4.0.0: + optional: true is-subdir@1.2.0: dependencies: @@ -6983,9 +6814,11 @@ snapshots: marked@4.3.0: {} - math-intrinsics@1.1.0: {} + math-intrinsics@1.1.0: + optional: true - media-typer@1.1.0: {} + media-typer@1.1.0: + optional: true memfs@4.51.1: dependencies: @@ -6996,7 +6829,8 @@ snapshots: tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 - merge-descriptors@2.0.0: {} + merge-descriptors@2.0.0: + optional: true merge-stream@2.0.0: {} @@ -7014,13 +6848,10 @@ snapshots: mime-types@3.0.1: dependencies: mime-db: 1.54.0 + optional: true mimic-function@5.0.1: {} - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.9 - minimatch@3.1.5: dependencies: brace-expansion: 1.1.18 @@ -7171,7 +7002,8 @@ snapshots: natural-compare@1.4.0: {} - negotiator@1.0.0: {} + negotiator@1.0.0: + optional: true neo-async@2.6.2: {} @@ -7206,13 +7038,15 @@ snapshots: object-assign@4.1.1: {} - object-inspect@1.13.4: {} + object-inspect@1.13.4: + optional: true obug@2.1.1: {} on-finished@2.4.1: dependencies: ee-first: 1.1.1 + optional: true once@1.4.0: dependencies: @@ -7327,7 +7161,8 @@ snapshots: dependencies: callsites: 3.1.0 - parseurl@1.3.3: {} + parseurl@1.3.3: + optional: true path-exists@4.0.0: {} @@ -7342,7 +7177,8 @@ snapshots: path-to-regexp@6.3.0: {} - path-to-regexp@8.4.0: {} + path-to-regexp@8.4.0: + optional: true path-type@4.0.0: {} @@ -7369,8 +7205,6 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - pluralize@8.0.0: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.19)(tsx@4.21.0)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 @@ -7414,18 +7248,21 @@ snapshots: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + optional: true punycode@2.3.1: {} qs@6.15.2: dependencies: side-channel: 1.1.0 + optional: true quansync@0.2.11: {} queue-microtask@1.2.3: {} - range-parser@1.2.1: {} + range-parser@1.2.1: + optional: true raw-body@3.0.2: dependencies: @@ -7433,6 +7270,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.2 unpipe: 1.0.0 + optional: true react-dom@19.2.4(react@19.2.4): dependencies: @@ -7536,6 +7374,7 @@ snapshots: path-to-regexp: 8.4.0 transitivePeerDependencies: - supports-color + optional: true run-parallel@1.2.0: dependencies: @@ -7545,7 +7384,8 @@ snapshots: dependencies: tslib: 2.8.1 - safe-buffer@5.2.1: {} + safe-buffer@5.2.1: + optional: true safer-buffer@2.1.2: {} @@ -7578,6 +7418,7 @@ snapshots: statuses: 2.0.2 transitivePeerDependencies: - supports-color + optional: true serve-static@2.2.0: dependencies: @@ -7587,10 +7428,12 @@ snapshots: send: 1.2.0 transitivePeerDependencies: - supports-color + optional: true set-cookie-parser@3.1.0: {} - setprototypeof@1.2.0: {} + setprototypeof@1.2.0: + optional: true shebang-command@2.0.0: dependencies: @@ -7609,6 +7452,7 @@ snapshots: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 + optional: true side-channel-map@1.0.1: dependencies: @@ -7616,6 +7460,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 + optional: true side-channel-weakmap@1.0.2: dependencies: @@ -7624,6 +7469,7 @@ snapshots: get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-map: 1.0.1 + optional: true side-channel@1.1.0: dependencies: @@ -7632,6 +7478,7 @@ snapshots: side-channel-list: 1.0.0 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + optional: true siginfo@2.0.0: {} @@ -7757,8 +7604,6 @@ snapshots: term-size@2.2.1: {} - termi-link@1.1.0: {} - terser-webpack-plugin@5.6.0(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.19)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.19)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -7815,7 +7660,8 @@ snapshots: dependencies: is-number: 7.0.0 - toidentifier@1.0.1: {} + toidentifier@1.0.1: + optional: true tough-cookie@6.0.1: dependencies: @@ -7964,6 +7810,7 @@ snapshots: content-type: 2.0.0 media-typer: 1.1.0 mime-types: 3.0.1 + optional: true typedoc-plugin-markdown@3.17.1(typedoc@0.25.13(typescript@5.4.4)): dependencies: @@ -7999,7 +7846,8 @@ snapshots: universalify@0.1.2: {} - unpipe@1.0.0: {} + unpipe@1.0.0: + optional: true unplugin@2.3.11: dependencies: @@ -8022,7 +7870,8 @@ snapshots: uuid@11.1.1: {} - vary@1.1.2: {} + vary@1.1.2: + optional: true vite-tsconfig-paths@4.3.2(typescript@5.4.4)(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): dependencies: diff --git a/turbo.json b/turbo.json index 6e5da8d56..6afc75def 100644 --- a/turbo.json +++ b/turbo.json @@ -98,38 +98,6 @@ "HUGGINGFACE_API_KEY" ], "dependsOn": ["^build", "build"] - }, - "playground:cli:push": { - "cache": false, - "passThroughEnv": [ - "ANTHROPIC_API_KEY", - "BRAINTRUST_API_KEY", - "GEMINI_API_KEY", - "COHERE_API_KEY", - "CURSOR_API_KEY", - "GROQ_API_KEY", - "OPENAI_API_KEY", - "OPENROUTER_API_KEY", - "MISTRAL_API_KEY", - "HUGGINGFACE_API_KEY" - ], - "dependsOn": ["^build", "build"] - }, - "playground:cli:eval": { - "cache": false, - "passThroughEnv": [ - "ANTHROPIC_API_KEY", - "BRAINTRUST_API_KEY", - "GEMINI_API_KEY", - "COHERE_API_KEY", - "CURSOR_API_KEY", - "GROQ_API_KEY", - "OPENAI_API_KEY", - "OPENROUTER_API_KEY", - "MISTRAL_API_KEY", - "HUGGINGFACE_API_KEY" - ], - "dependsOn": ["^build", "build"] } } } From ac1fde4e1d010cc44428ecceccd7ffa9fd6b04e7 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 20 Aug 2026 17:26:40 +0200 Subject: [PATCH 04/19] ref(eve)!: Bump minimum eve version to `0.34.0` (#2385) ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) --- .changeset/remove-eve-pre-034.md | 5 + e2e/config/pr-comment-scenarios.json | 2 +- .../eve-v0-latest.span-tree.json | 12 +- .../__snapshots__/eve-v0-latest.span-tree.txt | 12 +- .../__snapshots__/eve-v0.span-tree.json | 12 +- .../__snapshots__/eve-v0.span-tree.txt | 12 +- .../eve-instrumentation/agent/agent.ts | 10 +- .../eve-instrumentation/agent/channels/eve.ts | 7 + .../agent/hooks/braintrust.ts | 15 - .../braintrust.ts} | 8 +- .../agent/subagents/researcher/agent.ts | 3 + .../subagents/researcher/hooks/braintrust.ts | 15 - .../researcher/instrumentation/braintrust.ts | 18 + .../eve-instrumentation/cassette-filter.mjs | 114 +- .../eve-instrumentation/package.json | 6 +- .../eve-instrumentation/pnpm-lock.yaml | 89 +- .../eve-instrumentation/scenario.test.ts | 158 +- e2e/scenarios/eve-instrumentation/scenario.ts | 25 +- js/src/exports.ts | 5 +- js/src/instrumentation/index.ts | 5 +- .../plugins/eve-plugin.test.ts | 2929 ++++++---------- js/src/instrumentation/plugins/eve-plugin.ts | 3005 ++++++----------- js/src/vendor-sdk-types/eve.ts | 608 +--- js/src/wrappers/attachment-utils.ts | 20 + 24 files changed, 2488 insertions(+), 4607 deletions(-) create mode 100644 .changeset/remove-eve-pre-034.md create mode 100644 e2e/scenarios/eve-instrumentation/agent/channels/eve.ts delete mode 100644 e2e/scenarios/eve-instrumentation/agent/hooks/braintrust.ts rename e2e/scenarios/eve-instrumentation/agent/{instrumentation.ts => instrumentation/braintrust.ts} (66%) delete mode 100644 e2e/scenarios/eve-instrumentation/agent/subagents/researcher/hooks/braintrust.ts create mode 100644 e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts diff --git a/.changeset/remove-eve-pre-034.md b/.changeset/remove-eve-pre-034.md new file mode 100644 index 000000000..1e3034506 --- /dev/null +++ b/.changeset/remove-eve-pre-034.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref(eve)!: Bump minimum eve version to `0.34.0` diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index 4399ab7e3..6db8e7692 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -623,7 +623,7 @@ "label": "Eve Instrumentation", "metadataScenario": "eve-instrumentation", "variants": [ - { "variantKey": "eve-v0", "label": "v0 pinned" }, + { "variantKey": "eve-v0", "label": "v0.34 pinned" }, { "variantKey": "eve-v0-latest", "label": "v0 latest" } ] }, diff --git a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0-latest.span-tree.json b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0-latest.span-tree.json index f9869502c..c3eedeee9 100644 --- a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0-latest.span-tree.json +++ b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0-latest.span-tree.json @@ -53,6 +53,8 @@ ], "metadata": { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -233,8 +235,6 @@ "output": "Researcher result: [Eve instrumentation](https://eve.dev/docs/guides/instrumentation)", "metadata": { "eve.session_id": "", - "model": "qwen/qwen3-30b-a3b", - "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -365,6 +365,8 @@ ], "metadata": { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -525,6 +527,8 @@ ], "metadata": { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -705,8 +709,6 @@ "output": "Researcher result: Eve instrumentation https://eve.dev/docs/guides/instrumentation", "metadata": { "eve.session_id": "", - "model": "qwen/qwen3-30b-a3b", - "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -912,6 +914,8 @@ ], "metadata": { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, diff --git a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0-latest.span-tree.txt b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0-latest.span-tree.txt index 1006edff0..feec9910f 100644 --- a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0-latest.span-tree.txt +++ b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0-latest.span-tree.txt @@ -65,6 +65,8 @@ span_tree: │ │ ] │ │ metadata: { │ │ "eve.session_id": "", +│ │ "model": "qwen/qwen3-30b-a3b", +│ │ "provider": "openrouter", │ │ "scenario": "eve-instrumentation", │ │ "testRunId": "" │ │ } @@ -95,8 +97,6 @@ span_tree: │ │ output: "Researcher result: [Eve instrumentation](https://eve.dev/docs/guides/instrumentation)" │ │ metadata: { │ │ "eve.session_id": "", -│ │ "model": "qwen/qwen3-30b-a3b", -│ │ "provider": "openrouter", │ │ "scenario": "eve-instrumentation", │ │ "testRunId": "" │ │ } @@ -347,6 +347,8 @@ span_tree: │ ] │ metadata: { │ "eve.session_id": "", +│ "model": "qwen/qwen3-30b-a3b", +│ "provider": "openrouter", │ "scenario": "eve-instrumentation", │ "testRunId": "" │ } @@ -498,6 +500,8 @@ span_tree: │ ] │ metadata: { │ "eve.session_id": "", + │ "model": "qwen/qwen3-30b-a3b", + │ "provider": "openrouter", │ "scenario": "eve-instrumentation", │ "testRunId": "" │ } @@ -528,8 +532,6 @@ span_tree: │ output: "Researcher result: Eve instrumentation https://eve.dev/docs/guides/instrumentation" │ metadata: { │ "eve.session_id": "", - │ "model": "qwen/qwen3-30b-a3b", - │ "provider": "openrouter", │ "scenario": "eve-instrumentation", │ "testRunId": "" │ } @@ -855,6 +857,8 @@ span_tree: ] metadata: { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" } diff --git a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0.span-tree.json b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0.span-tree.json index 6066e92ae..af1d20932 100644 --- a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0.span-tree.json +++ b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0.span-tree.json @@ -53,6 +53,8 @@ ], "metadata": { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -233,8 +235,6 @@ "output": "Researcher result: {\"title\": \"Eve instrumentation\", \"url\": \"https://eve.dev/docs/guides/instrumentation\"}", "metadata": { "eve.session_id": "", - "model": "qwen/qwen3-30b-a3b", - "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -365,6 +365,8 @@ ], "metadata": { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -525,6 +527,8 @@ ], "metadata": { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -705,8 +709,6 @@ "output": "Researcher result: Eve instrumentation (https://eve.dev/docs/guides/instrumentation)", "metadata": { "eve.session_id": "", - "model": "qwen/qwen3-30b-a3b", - "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, @@ -912,6 +914,8 @@ ], "metadata": { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" }, diff --git a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0.span-tree.txt b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0.span-tree.txt index d86328682..1a17de807 100644 --- a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0.span-tree.txt +++ b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-v0.span-tree.txt @@ -65,6 +65,8 @@ span_tree: │ │ ] │ │ metadata: { │ │ "eve.session_id": "", +│ │ "model": "qwen/qwen3-30b-a3b", +│ │ "provider": "openrouter", │ │ "scenario": "eve-instrumentation", │ │ "testRunId": "" │ │ } @@ -95,8 +97,6 @@ span_tree: │ │ output: "Researcher result: {\"title\": \"Eve instrumentation\", \"url\": \"https://eve.dev/docs/guides/instrumentation\"}" │ │ metadata: { │ │ "eve.session_id": "", -│ │ "model": "qwen/qwen3-30b-a3b", -│ │ "provider": "openrouter", │ │ "scenario": "eve-instrumentation", │ │ "testRunId": "" │ │ } @@ -347,6 +347,8 @@ span_tree: │ ] │ metadata: { │ "eve.session_id": "", +│ "model": "qwen/qwen3-30b-a3b", +│ "provider": "openrouter", │ "scenario": "eve-instrumentation", │ "testRunId": "" │ } @@ -498,6 +500,8 @@ span_tree: │ ] │ metadata: { │ "eve.session_id": "", + │ "model": "qwen/qwen3-30b-a3b", + │ "provider": "openrouter", │ "scenario": "eve-instrumentation", │ "testRunId": "" │ } @@ -528,8 +532,6 @@ span_tree: │ output: "Researcher result: Eve instrumentation (https://eve.dev/docs/guides/instrumentation)" │ metadata: { │ "eve.session_id": "", - │ "model": "qwen/qwen3-30b-a3b", - │ "provider": "openrouter", │ "scenario": "eve-instrumentation", │ "testRunId": "" │ } @@ -855,6 +857,8 @@ span_tree: ] metadata: { "eve.session_id": "", + "model": "qwen/qwen3-30b-a3b", + "provider": "openrouter", "scenario": "eve-instrumentation", "testRunId": "" } diff --git a/e2e/scenarios/eve-instrumentation/agent/agent.ts b/e2e/scenarios/eve-instrumentation/agent/agent.ts index 589c856cc..8b518295b 100644 --- a/e2e/scenarios/eve-instrumentation/agent/agent.ts +++ b/e2e/scenarios/eve-instrumentation/agent/agent.ts @@ -18,11 +18,15 @@ const dynamicModel = withReadableReasoning( ); export default defineAgent({ + experimental: { + instrumentationProviders: true, + }, model: defineDynamic({ - fallback: dynamicModel, events: { - "step.started": () => dynamicModel, + "step.started": () => ({ + model: dynamicModel, + modelContextWindowTokens: 8_192, + }), }, }), - modelContextWindowTokens: 8_192, }); diff --git a/e2e/scenarios/eve-instrumentation/agent/channels/eve.ts b/e2e/scenarios/eve-instrumentation/agent/channels/eve.ts new file mode 100644 index 000000000..b74a16488 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/channels/eve.ts @@ -0,0 +1,7 @@ +import { none } from "eve/channels/auth"; +import { eveChannel } from "eve/channels/eve"; + +export default eveChannel({ + // This local e2e fixture has no user accounts and only serves the test runner. + auth: none(), +}); diff --git a/e2e/scenarios/eve-instrumentation/agent/hooks/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/hooks/braintrust.ts deleted file mode 100644 index fcac75fde..000000000 --- a/e2e/scenarios/eve-instrumentation/agent/hooks/braintrust.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { braintrustEveHook } from "braintrust"; -import { defineState } from "eve/context"; -import { defineHook } from "eve/hooks"; - -export default defineHook( - braintrustEveHook({ - defineState, - metadata: { - scenario: "eve-instrumentation", - ...(process.env.BRAINTRUST_E2E_RUN_ID - ? { testRunId: process.env.BRAINTRUST_E2E_RUN_ID } - : {}), - }, - }), -); diff --git a/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts b/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts similarity index 66% rename from e2e/scenarios/eve-instrumentation/agent/instrumentation.ts rename to e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts index fff8a737d..9ba5484f2 100644 --- a/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts +++ b/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts @@ -1,10 +1,14 @@ import { braintrustEveInstrumentation, initLogger } from "braintrust"; -import { defineState } from "eve/context"; import { defineInstrumentation } from "eve/instrumentation"; export default defineInstrumentation( braintrustEveInstrumentation({ - defineState, + metadata: { + scenario: "eve-instrumentation", + ...(process.env.BRAINTRUST_E2E_RUN_ID + ? { testRunId: process.env.BRAINTRUST_E2E_RUN_ID } + : {}), + }, setup: ({ agentName }) => { initLogger({ projectName: process.env.BRAINTRUST_E2E_PROJECT_NAME || agentName, diff --git a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/agent.ts b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/agent.ts index 955d05a88..158110861 100644 --- a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/agent.ts +++ b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/agent.ts @@ -11,6 +11,9 @@ const openrouter = createOpenRouter({ export default defineAgent({ description: "Research the Eve instrumentation documentation before the parent reads it.", + experimental: { + instrumentationProviders: true, + }, model: withReadableReasoning( openrouter("qwen/qwen3-30b-a3b", { provider: { diff --git a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/hooks/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/hooks/braintrust.ts deleted file mode 100644 index fcac75fde..000000000 --- a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/hooks/braintrust.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { braintrustEveHook } from "braintrust"; -import { defineState } from "eve/context"; -import { defineHook } from "eve/hooks"; - -export default defineHook( - braintrustEveHook({ - defineState, - metadata: { - scenario: "eve-instrumentation", - ...(process.env.BRAINTRUST_E2E_RUN_ID - ? { testRunId: process.env.BRAINTRUST_E2E_RUN_ID } - : {}), - }, - }), -); diff --git a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts new file mode 100644 index 000000000..9ba5484f2 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts @@ -0,0 +1,18 @@ +import { braintrustEveInstrumentation, initLogger } from "braintrust"; +import { defineInstrumentation } from "eve/instrumentation"; + +export default defineInstrumentation( + braintrustEveInstrumentation({ + metadata: { + scenario: "eve-instrumentation", + ...(process.env.BRAINTRUST_E2E_RUN_ID + ? { testRunId: process.env.BRAINTRUST_E2E_RUN_ID } + : {}), + }, + setup: ({ agentName }) => { + initLogger({ + projectName: process.env.BRAINTRUST_E2E_PROJECT_NAME || agentName, + }); + }, + }), +); diff --git a/e2e/scenarios/eve-instrumentation/cassette-filter.mjs b/e2e/scenarios/eve-instrumentation/cassette-filter.mjs index f73d1a5ef..4066cef31 100644 --- a/e2e/scenarios/eve-instrumentation/cassette-filter.mjs +++ b/e2e/scenarios/eve-instrumentation/cassette-filter.mjs @@ -1,5 +1,117 @@ // @ts-check -export { filter } from "../ai-sdk-instrumentation/cassette-filter.mjs"; +const EVE_BUILTIN_TOOLS = new Set([ + "agent", + "ask_question", + "bash", + "glob", + "grep", + "load_skill", + "read_file", + "todo", + "web_fetch", + "write_file", +]); + +/** @type {import("@braintrust/seinfeld").FilterSpec} */ +export const filter = [ + "default", + { + ignoreBodyFields: [ + "experimental_generateMessageId", + "messageId", + "messages.*.id", + "messages.*.experimental_messageId", + // Eve changed the runtime-owned subagent output-schema declaration + // without changing this fixture's authored tool contract. + "tools.*.function.parameters.properties.outputSchema", + // JSON Schema's draft marker is metadata, not part of the accepted input. + "tools.*.function.parameters.$schema", + ], + normalizeRequest(request) { + if ( + request.body.kind !== "json" || + request.body.value === null || + typeof request.body.value !== "object" || + Array.isArray(request.body.value) + ) { + return request; + } + const body = request.body.value; + if (!Array.isArray(body.tools)) { + return request; + } + const messages = Array.isArray(body.messages) + ? body.messages.map((message) => message) + : body.messages; + if (Array.isArray(messages)) { + for (let index = 0; index < messages.length; index++) { + const message = messages[index]; + if ( + message === null || + typeof message !== "object" || + !("role" in message) || + message.role !== "tool" + ) { + continue; + } + let end = index + 1; + while ( + end < messages.length && + messages[end] !== null && + typeof messages[end] === "object" && + "role" in messages[end] && + messages[end].role === "tool" + ) { + end++; + } + messages.splice( + index, + end - index, + ...messages.slice(index, end).sort((left, right) => { + const leftName = + left !== null && + typeof left === "object" && + "name" in left && + typeof left.name === "string" + ? left.name + : ""; + const rightName = + right !== null && + typeof right === "object" && + "name" in right && + typeof right.name === "string" + ? right.name + : ""; + return leftName.localeCompare(rightName); + }), + ); + index = end - 1; + } + } + return { + ...request, + body: { + kind: "json", + value: { + ...body, + messages, + tools: body.tools.filter( + (tool) => + tool === null || + typeof tool !== "object" || + !("function" in tool) || + tool.function === null || + typeof tool.function !== "object" || + !("name" in tool.function) || + typeof tool.function.name !== "string" || + !EVE_BUILTIN_TOOLS.has(tool.function.name), + ), + }, + }, + }; + }, + }, +]; /** @type {import("@braintrust/seinfeld").RedactionSpec} */ export const redact = [ diff --git a/e2e/scenarios/eve-instrumentation/package.json b/e2e/scenarios/eve-instrumentation/package.json index 202841885..93b789343 100644 --- a/e2e/scenarios/eve-instrumentation/package.json +++ b/e2e/scenarios/eve-instrumentation/package.json @@ -14,9 +14,9 @@ }, "dependencies": { "@openrouter/ai-sdk-provider": "3.0.0", - "ai": "7.0.34", - "eve-v0": "npm:eve@0.22.1", - "eve-v0-latest": "npm:eve@0.27.6", + "ai": "7.0.68", + "eve-v0": "npm:eve@0.34.0", + "eve-v0-latest": "npm:eve@0.39.1", "zod": "4.3.6" } } diff --git a/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml b/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml index c3cbc645d..e6b942a7a 100644 --- a/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml +++ b/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml @@ -10,36 +10,36 @@ importers: dependencies: '@openrouter/ai-sdk-provider': specifier: 3.0.0 - version: 3.0.0(ai@7.0.34(zod@4.3.6))(zod@4.3.6) + version: 3.0.0(ai@7.0.68(zod@4.3.6))(zod@4.3.6) ai: - specifier: 7.0.34 - version: 7.0.34(zod@4.3.6) + specifier: 7.0.68 + version: 7.0.68(zod@4.3.6) eve-v0: - specifier: npm:eve@0.22.1 - version: eve@0.22.1(ai@7.0.34(zod@4.3.6)) + specifier: npm:eve@0.34.0 + version: eve@0.34.0(ai@7.0.68(zod@4.3.6)) eve-v0-latest: - specifier: npm:eve@0.27.6 - version: eve@0.27.6(ai@7.0.34(zod@4.3.6)) + specifier: npm:eve@0.39.1 + version: eve@0.39.1(ai@7.0.68(zod@4.3.6)) zod: specifier: 4.3.6 version: 4.3.6 packages: - '@ai-sdk/gateway@4.0.26': - resolution: {integrity: sha512-b/nc3COKtk8IxzgcCi418IoZFky/Bw+Jg8+J0/SBBnu/mOXA4bkIKCu81coEtJAWuH2g5Fvvsa0ar7EpmzNWTw==} + '@ai-sdk/gateway@4.0.54': + resolution: {integrity: sha512-x4fAXDqCtYzB/M5vsIQLYcyrzpJuaRgcIwDSw+lpTMMbgH19fU3ds75GSlHNLzfx6Z5yL4Z9+EMr0GJcqVy9QA==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@5.0.12': - resolution: {integrity: sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA==} + '@ai-sdk/provider-utils@5.0.27': + resolution: {integrity: sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@4.0.3': - resolution: {integrity: sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==} + '@ai-sdk/provider@4.0.7': + resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==} engines: {node: '>=22'} '@emnapi/core@1.11.1': @@ -178,8 +178,8 @@ packages: '@workflow/serde@4.1.0': resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} - ai@7.0.34: - resolution: {integrity: sha512-jDqclWYqPGFKcUG4CQiKcBiwi5XqVMqvYy6eJdujVpvE1SDoB6j7RtjrGYz3Bn5B1dzTj1StAlrOebY//WK9/Q==} + ai@7.0.68: + resolution: {integrity: sha512-9QuZOT77wzoxxUC0NcueXhCo3HUHA/1pApIJ9VRyE+9/K+3Innkq4kVhrd9aEnwIviJz2Nga063m+UTsPSdOyw==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -237,13 +237,13 @@ packages: wrangler: optional: true - eve@0.22.1: - resolution: {integrity: sha512-4nqy8SEJpS94fM8sxd0Or+irvf/CmS0qENxouKyyegst3Qtn06xZbuSEoI1p7i4wB6sprr95udXh7Ulct6ABrA==} + eve@0.34.0: + resolution: {integrity: sha512-zgrYIbm8u78Furxfdyzaknf5AxFCzwmTGfLEvgax00BUNXu3eebaos8igjYQyM4q5JAxlWAoZCJuItE6r2S0cw==} engines: {node: '>=24'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 - ai: ^7.0.0 + ai: ^7.0.58 braintrust: ^3.0.0 just-bash: ^3.0.0 microsandbox: ^0.5.0 @@ -257,13 +257,13 @@ packages: microsandbox: optional: true - eve@0.27.6: - resolution: {integrity: sha512-t2Xr/nlVGc0+Fy9fav8p/CJrwMSQM0zyuOGY1F1XapN0UFssGYBGa6Ml/Q5OnowvWwsGH4m0Tf2QAccayVCvtw==} + eve@0.39.1: + resolution: {integrity: sha512-HTd1A3/GsRKNpJ+6WEW4OEPAhWAId5IZ769hjuY3eJqj3cWeaEvBUDZSjUFjSDit63aqbfur0u7AH1m0JNr7EQ==} engines: {node: '>=24'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 - ai: ^7.0.34 + ai: ^7.0.58 braintrust: ^3.0.0 just-bash: ^3.0.0 microsandbox: ^0.5.0 @@ -365,6 +365,14 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -447,22 +455,23 @@ packages: snapshots: - '@ai-sdk/gateway@4.0.26(zod@4.3.6)': + '@ai-sdk/gateway@4.0.54(zod@4.3.6)': dependencies: - '@ai-sdk/provider': 4.0.3 - '@ai-sdk/provider-utils': 5.0.12(zod@4.3.6) + '@ai-sdk/provider': 4.0.7 + '@ai-sdk/provider-utils': 5.0.27(zod@4.3.6) '@vercel/oidc': 3.2.0 zod: 4.3.6 - '@ai-sdk/provider-utils@5.0.12(zod@4.3.6)': + '@ai-sdk/provider-utils@5.0.27(zod@4.3.6)': dependencies: - '@ai-sdk/provider': 4.0.3 + '@ai-sdk/provider': 4.0.7 '@standard-schema/spec': 1.1.0 '@workflow/serde': 4.1.0 eventsource-parser: 3.1.0 + undici: 7.29.0 zod: 4.3.6 - '@ai-sdk/provider@4.0.3': + '@ai-sdk/provider@4.0.7': dependencies: json-schema: 0.4.0 @@ -489,9 +498,9 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@openrouter/ai-sdk-provider@3.0.0(ai@7.0.34(zod@4.3.6))(zod@4.3.6)': + '@openrouter/ai-sdk-provider@3.0.0(ai@7.0.68(zod@4.3.6))(zod@4.3.6)': dependencies: - ai: 7.0.34(zod@4.3.6) + ai: 7.0.68(zod@4.3.6) zod: 4.3.6 '@oxc-project/types@0.138.0': {} @@ -558,11 +567,11 @@ snapshots: '@workflow/serde@4.1.0': {} - ai@7.0.34(zod@4.3.6): + ai@7.0.68(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 4.0.26(zod@4.3.6) - '@ai-sdk/provider': 4.0.3 - '@ai-sdk/provider-utils': 5.0.12(zod@4.3.6) + '@ai-sdk/gateway': 4.0.54(zod@4.3.6) + '@ai-sdk/provider': 4.0.7 + '@ai-sdk/provider-utils': 5.0.27(zod@4.3.6) zod: 4.3.6 consola@3.4.2: {} @@ -580,10 +589,11 @@ snapshots: httpxy: 0.5.4 srvx: 0.11.21 - eve@0.22.1(ai@7.0.34(zod@4.3.6)): + eve@0.34.0(ai@7.0.68(zod@4.3.6)): dependencies: - ai: 7.0.34(zod@4.3.6) + ai: 7.0.68(zod@4.3.6) nitro: 3.0.260610-beta + undici: 8.9.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -624,10 +634,11 @@ snapshots: - xml2js - zephyr-agent - eve@0.27.6(ai@7.0.34(zod@4.3.6)): + eve@0.39.1(ai@7.0.68(zod@4.3.6)): dependencies: - ai: 7.0.34(zod@4.3.6) + ai: 7.0.68(zod@4.3.6) nitro: 3.0.260610-beta + undici: 8.9.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -773,6 +784,10 @@ snapshots: tslib@2.8.1: optional: true + undici@7.29.0: {} + + undici@8.9.0: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 diff --git a/e2e/scenarios/eve-instrumentation/scenario.test.ts b/e2e/scenarios/eve-instrumentation/scenario.test.ts index 4d65888c9..2fcd1ad48 100644 --- a/e2e/scenarios/eve-instrumentation/scenario.test.ts +++ b/e2e/scenarios/eve-instrumentation/scenario.test.ts @@ -26,7 +26,7 @@ const eveScenarios = await Promise.all( [ { dependencyName: "eve-v0", - label: "v0 pinned", + label: "v0.34 pinned", variantKey: "eve-v0", }, { @@ -137,12 +137,16 @@ describe.sequential("eve instrumentation variants", () => { "read", secondRoot?.span.id, ); - + const secondChildSearch = findLatestChildSpan( + events, + "search", + secondChildTurn?.span.id, + ); expect(findAllSpans(events, "eve.session")).toEqual([]); expect(turns).toHaveLength(4); expect( turns.filter((turn) => turn.span.parentIds.length === 0), - ).toHaveLength(2); + ).toEqual([root, secondRoot]); expect(new Set(turns.map((turn) => turn.span.rootId)).size).toBe(2); expect(root).toBeDefined(); @@ -155,10 +159,21 @@ describe.sequential("eve instrumentation variants", () => { }); expect(root?.metadata).not.toHaveProperty("model"); expect(root?.metadata).not.toHaveProperty("provider"); - expect(root?.metrics?.completion_tokens).toEqual(expect.any(Number)); - expect(root?.metrics?.prompt_tokens).toEqual(expect.any(Number)); - expect(root?.metrics?.tokens).toEqual(expect.any(Number)); expect(root?.output).toContain("Final answer from read"); + expect(root?.metrics).toMatchObject({ + completion_tokens: steps.reduce( + (total, step) => total + (step.metrics?.completion_tokens ?? 0), + 0, + ), + prompt_tokens: steps.reduce( + (total, step) => total + (step.metrics?.prompt_tokens ?? 0), + 0, + ), + tokens: steps.reduce( + (total, step) => total + (step.metrics?.tokens ?? 0), + 0, + ), + }); expect(steps).toHaveLength(2); expect(steps.map((step) => step.span.type)).toEqual(["llm", "llm"]); @@ -191,11 +206,14 @@ describe.sequential("eve instrumentation variants", () => { expect(step.input[0]).toMatchObject({ role: "system" }); expect(step.metadata).toMatchObject({ "eve.session_id": root?.metadata?.["eve.session_id"], + model: "qwen/qwen3-30b-a3b", + provider: "openrouter", scenario: "eve-instrumentation", testRunId: expect.any(String), }); - expect(step.metadata).not.toHaveProperty("model"); - expect(step.metadata).not.toHaveProperty("provider"); + expect(step.metrics?.completion_tokens).toEqual(expect.any(Number)); + expect(step.metrics?.prompt_tokens).toEqual(expect.any(Number)); + expect(step.metrics?.tokens).toEqual(expect.any(Number)); } expect(researcher).toBeDefined(); @@ -217,14 +235,28 @@ describe.sequential("eve instrumentation variants", () => { expect(childTurn?.span.rootId).toEqual(root?.span.rootId); expect(childTurn?.metadata).toMatchObject({ "eve.session_id": expect.any(String), - model: "qwen/qwen3-30b-a3b", - provider: "openrouter", scenario: "eve-instrumentation", testRunId: expect.any(String), }); + expect(childTurn?.metadata).not.toHaveProperty("model"); + expect(childTurn?.metadata).not.toHaveProperty("provider"); expect(childTurn?.metadata?.["eve.session_id"]).not.toEqual( root?.metadata?.["eve.session_id"], ); + expect(childTurn?.metrics).toMatchObject({ + completion_tokens: childSteps.reduce( + (total, step) => total + (step.metrics?.completion_tokens ?? 0), + 0, + ), + prompt_tokens: childSteps.reduce( + (total, step) => total + (step.metrics?.prompt_tokens ?? 0), + 0, + ), + tokens: childSteps.reduce( + (total, step) => total + (step.metrics?.tokens ?? 0), + 0, + ), + }); expect(childSteps).toHaveLength(2); for (const step of childSteps) { @@ -242,6 +274,9 @@ describe.sequential("eve instrumentation variants", () => { scenario: "eve-instrumentation", testRunId: expect.any(String), }); + expect(step.metrics?.completion_tokens).toEqual(expect.any(Number)); + expect(step.metrics?.prompt_tokens).toEqual(expect.any(Number)); + expect(step.metrics?.tokens).toEqual(expect.any(Number)); expect(step.output).toMatchObject([ { message: { @@ -286,74 +321,81 @@ describe.sequential("eve instrumentation variants", () => { expect(secondRoot?.span.rootId).not.toEqual(root?.span.rootId); expect(secondRoot?.metadata).toMatchObject({ "eve.session_id": root?.metadata?.["eve.session_id"], + scenario: "eve-instrumentation", + testRunId: expect.any(String), }); - expect(secondRoot?.metadata).not.toHaveProperty("model"); - expect(secondRoot?.metadata).not.toHaveProperty("provider"); expect(secondRoot?.output).toContain("Final answer from read"); + expect(secondRoot?.metrics).toMatchObject({ + completion_tokens: secondSteps.reduce( + (total, step) => total + (step.metrics?.completion_tokens ?? 0), + 0, + ), + prompt_tokens: secondSteps.reduce( + (total, step) => total + (step.metrics?.prompt_tokens ?? 0), + 0, + ), + tokens: secondSteps.reduce( + (total, step) => total + (step.metrics?.tokens ?? 0), + 0, + ), + }); expect(secondSteps).toHaveLength(2); - expect(secondSteps.map((step) => step.span.type)).toEqual([ - "llm", - "llm", - ]); for (const step of secondSteps) { + expect(step.span.parentIds).toEqual([secondRoot?.span.id]); + expect(step.span.rootId).toEqual(secondRoot?.span.rootId); expect(step.metadata).toMatchObject({ "eve.session_id": secondRoot?.metadata?.["eve.session_id"], - scenario: "eve-instrumentation", - testRunId: expect.any(String), + model: "qwen/qwen3-30b-a3b", + provider: "openrouter", }); - expect(step.metadata).not.toHaveProperty("model"); - expect(step.metadata).not.toHaveProperty("provider"); } - expect(secondSteps[0]?.output).toMatchObject([ - { - finish_reason: "tool_calls", - message: { - reasoning: [{ content: expect.any(String) }], - tool_calls: [ - { function: { name: "researcher" }, type: "function" }, - { function: { name: "read" }, type: "function" }, - ], - }, - }, - ]); - expect(secondSteps[1]?.output).toMatchObject([ - { - finish_reason: "stop", - message: { - reasoning: [{ content: expect.any(String) }], - }, - }, - ]); - expect(secondResearcher?.span.type).toBe("tool"); - expect(secondResearcher?.span.ended).toBe(true); expect(secondResearcher?.span.parentIds).toEqual([secondRoot?.span.id]); - expect(secondResearcher?.metadata).toMatchObject({ - "eve.session_id": secondRoot?.metadata?.["eve.session_id"], - }); expect(secondChildTurn?.span.parentIds).toEqual([ secondResearcher?.span.id, ]); expect(secondChildTurn?.span.rootId).toEqual(secondRoot?.span.rootId); - expect(secondChildTurn?.metadata).toMatchObject({ - "eve.session_id": expect.any(String), - model: "qwen/qwen3-30b-a3b", - provider: "openrouter", - }); - expect(secondChildTurn?.metadata?.["eve.session_id"]).not.toEqual( - secondRoot?.metadata?.["eve.session_id"], - ); - expect(secondRead?.span.type).toBe("tool"); - expect(secondRead?.span.ended).toBe(true); + expect(secondChildTurn?.span.rootId).not.toEqual(root?.span.rootId); expect(secondRead?.span.parentIds).toEqual([secondRoot?.span.id]); - expect(secondRead?.metadata).toMatchObject({ - "eve.session_id": secondRoot?.metadata?.["eve.session_id"], - }); for (const event of events) { expect(spanInstrumentationName(event)).toBe("eve"); } const rawRows = payloads.flatMap((payload) => payload.rows); + for (const span of [ + ...findAllSpans(events, "eve.step"), + researcher, + childSearch, + read, + secondResearcher, + secondRead, + secondChildSearch, + ]) { + expect(span).toBeDefined(); + expect( + rawRows.filter( + (row) => + row.id === span?.row.id && + Object.prototype.hasOwnProperty.call(row, "input"), + ), + ).toHaveLength(1); + expect( + rawRows.filter( + (row) => + row.id === span?.row.id && + Object.prototype.hasOwnProperty.call(row, "metadata"), + ), + ).toHaveLength(1); + } + for (const turn of turns) { + expect( + rawRows.filter( + (row) => + row.id === turn.row.id && + Object.prototype.hasOwnProperty.call(row, "metadata"), + ), + ).toHaveLength(1); + } for (const step of findAllSpans(events, "eve.step")) { expect( rawRows.filter( diff --git a/e2e/scenarios/eve-instrumentation/scenario.ts b/e2e/scenarios/eve-instrumentation/scenario.ts index 48efc7bea..ca1c92027 100644 --- a/e2e/scenarios/eve-instrumentation/scenario.ts +++ b/e2e/scenarios/eve-instrumentation/scenario.ts @@ -65,13 +65,10 @@ async function main() { } const body = (await response.json()) as { - continuationToken?: string; sessionId?: string; }; - if (!body.sessionId || !body.continuationToken) { - throw new Error( - `Eve session create did not return a sessionId and continuationToken`, - ); + if (!body.sessionId) { + throw new Error(`Eve session create did not return a sessionId`); } const seenSessionIds = new Set([body.sessionId]); @@ -81,11 +78,13 @@ async function main() { seenSessionIds, "session.waiting", ); + // Eve emits session.waiting just before its durable session snapshot is + // visible to the continuation route. + await new Promise((resolve) => setTimeout(resolve, 1000)); const followUp = await fetch( `${baseUrl}/eve/v1/session/${body.sessionId}`, { body: JSON.stringify({ - continuationToken: body.continuationToken, message: "Run the Braintrust Eve instrumentation e2e scenario again", }), headers: { "content-type": "application/json" }, @@ -213,6 +212,7 @@ async function streamUntil( const decoder = new TextDecoder(); let buffer = ""; let nextIndex = startIndex; + let turnCompleted = false; try { while (true) { const { done, value } = await reader.read(); @@ -228,7 +228,10 @@ async function streamUntil( continue; } const event = JSON.parse(trimmed) as { - data?: { childSessionId?: string; message?: string }; + data?: { + childSessionId?: string; + message?: string; + }; type?: string; }; nextIndex++; @@ -256,7 +259,13 @@ async function streamUntil( ).then(() => undefined), ); } - if (event.type === until) { + if (event.type === "turn.completed") { + turnCompleted = true; + } + if ( + event.type === until && + (until !== "session.waiting" || turnCompleted) + ) { await Promise.all(childStreams); return nextIndex; } diff --git a/js/src/exports.ts b/js/src/exports.ts index d39c15459..9ed496ceb 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -196,10 +196,7 @@ export { BraintrustMiddleware, wrapAISDKModel, } from "./wrappers/ai-sdk"; -export { - braintrustEveHook, - braintrustEveInstrumentation, -} from "./instrumentation/plugins/eve-plugin"; +export { braintrustEveInstrumentation } from "./instrumentation/plugins/eve-plugin"; export { collectAnthropicSession } from "./wrappers/anthropic-session-collector"; export { wrapAnthropic } from "./wrappers/anthropic"; export { diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index ce4352ebc..8bbbd31ec 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -20,10 +20,7 @@ export type { BraintrustPluginConfig } from "./braintrust-plugin"; export { OpenAIAgentsTraceProcessor } from "./plugins/openai-agents-trace-processor"; export type { OpenAIAgentsTraceProcessorOptions } from "./plugins/openai-agents-trace-processor"; export { braintrustFlueInstrumentation } from "./plugins/flue-plugin"; -export { - braintrustEveHook, - braintrustEveInstrumentation, -} from "./plugins/eve-plugin"; +export { braintrustEveInstrumentation } from "./plugins/eve-plugin"; // Re-export core types for external instrumentation packages export type { diff --git a/js/src/instrumentation/plugins/eve-plugin.test.ts b/js/src/instrumentation/plugins/eve-plugin.test.ts index bf4323b71..38e15bef0 100644 --- a/js/src/instrumentation/plugins/eve-plugin.test.ts +++ b/js/src/instrumentation/plugins/eve-plugin.test.ts @@ -7,46 +7,38 @@ import { it, vi, } from "vitest"; -import { createHash } from "node:crypto"; import * as braintrustExports from "../../exports"; import { configureNode } from "../../node/config"; -import { - _exportsForTestingOnly, - initLogger, - startSpan, - withCurrent, -} from "../../logger"; +import { Attachment, _exportsForTestingOnly, initLogger } from "../../logger"; import * as instrumentationExports from "../index"; -import { braintrustEveHook, braintrustEveInstrumentation } from "./eve-plugin"; +import { braintrustEveInstrumentation } from "./eve-plugin"; import type { - EveHandleMessageStreamEvent, - EveHookContext, + EveInstrumentationAttemptScope, + EveInstrumentationHandlerContext, + EveInstrumentationModelCallCompletedEvent, + EveJsonValue, } from "../../vendor-sdk-types/eve"; -import { mergeRowBatch } from "../../../util/index"; - -function deterministicEveIdForTest(...parts: string[]): string { - return createHash("sha256") - .update(parts.map((part) => `${part.length}:${part}`).join("\0")) - .digest("hex") - .slice(0, 32) - .replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5"); -} -function createFakeDefineState() { - const values = new Map(); +const ATTEMPT_SCOPE: EveInstrumentationAttemptScope = { + attemptId: "attempt-0", + attemptIndex: 0, + rootSessionId: "session-root", + sessionId: "session-root", + stepIndex: 0, + turnId: "turn-0", +}; + +function providerContext( + initial?: EveJsonValue, +): EveInstrumentationHandlerContext { + let value = initial; return { - defineState(name: string, initial: () => T) { - return { - get: () => (values.has(name) ? (values.get(name) as T) : initial()), - update: (fn: (current: T) => T) => { - values.set( - name, - fn(values.has(name) ? (values.get(name) as T) : initial()), - ); - }, - }; + state: { + get: () => value, + set: (next) => { + value = next; + }, }, - values, }; } @@ -56,18 +48,16 @@ try { // Best-effort initialization for test environments. } -describe("braintrustEveHook", () => { +describe("braintrustEveInstrumentation", () => { let backgroundLogger: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; - let defineState: ReturnType["defineState"]; beforeAll(async () => { await _exportsForTestingOnly.simulateLoginForTests(); }); beforeEach(() => { - defineState = createFakeDefineState().defineState; backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ projectName: "eve-plugin.test.ts", @@ -80,517 +70,186 @@ describe("braintrustEveHook", () => { _exportsForTestingOnly.clearTestBackgroundLogger(); }); - it("returns an Eve hook definition", () => { - const hook = braintrustEveHook({ defineState }); - - expect(Object.keys(hook)).toEqual(["events"]); - expect(typeof hook.events?.["*"]).toBe("function"); - }); - - it("returns an Eve instrumentation definition", () => { + it("returns an Eve 0.34+ instrumentation provider", () => { const setup = vi.fn(); - const instrumentation = braintrustEveInstrumentation({ - defineState, - setup, - }); - - expect(instrumentation).toMatchObject({ - recordInputs: false, - recordOutputs: false, - setup, - }); - expect(typeof instrumentation.events?.["step.started"]).toBe("function"); - }); - - it("requires Eve's defineState API", () => { - expect(() => braintrustEveHook(undefined as never)).toThrow(); - expect(() => braintrustEveInstrumentation(undefined as never)).toThrow(); + const provider = braintrustEveInstrumentation({ setup }); + + expect(provider.capture).toBe("content"); + expect(provider.setup).toBe(setup); + expect(typeof provider.flush).toBe("function"); + expect(Object.keys(provider.events).sort()).toEqual([ + "action.completed", + "action.failed", + "action.started", + "model.call.completed", + "model.call.failed", + "model.call.started", + "step.attempt.completed", + "step.attempt.failed", + "turn.cancelled", + "turn.completed", + "turn.failed", + "turn.started", + ]); }); - it("exports Eve APIs from root and instrumentation entrypoints", () => { - expect(braintrustExports.braintrustEveHook).toBe(braintrustEveHook); + it("exports only the Eve instrumentation provider API", () => { expect(braintrustExports.braintrustEveInstrumentation).toBe( braintrustEveInstrumentation, ); - expect(instrumentationExports.braintrustEveHook).toBe(braintrustEveHook); expect(instrumentationExports.braintrustEveInstrumentation).toBe( braintrustEveInstrumentation, ); + expect(braintrustExports).not.toHaveProperty("braintrustEveHook"); + expect(instrumentationExports).not.toHaveProperty("braintrustEveHook"); }); - it("captures stable model input without provider options", async () => { - const fakeEve = createFakeDefineState(); - defineState = fakeEve.defineState; - const instrumentation = braintrustEveInstrumentation({ defineState }); - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - expect(wildcard).toBeDefined(); - - const ctx: EveHookContext = { - session: { id: "session-captured-input" }, - }; - const emit = (event: EveHandleMessageStreamEvent) => wildcard?.(event, ctx); - const modelInput = { - instructions: [ - { - content: "Answer with the relevant Eve instrumentation detail.", - providerOptions: { - openai: { reasoningEncryptedContent: "system-provider-secret" }, - }, - role: "system", - }, - { - content: "Keep the answer concise.", - role: "system", - }, - ], - messages: [ - { - content: [ - { - providerOptions: { - openai: { - itemId: "reasoning-item", - reasoningEncryptedContent: "must-not-be-logged", - }, - }, - text: "Readable reasoning", - type: "reasoning", - }, - { - input: { - providerOptions: { application: "tool-input" }, - reasoningEncryptedContent: "legitimate-tool-input", - }, - providerOptions: { - openai: { reasoningEncryptedContent: "tool-provider-secret" }, - }, - toolCallId: "call-1", - toolName: "lookup", - type: "tool-call", - }, - { - output: { - providerOptions: { - openai: { - reasoningEncryptedContent: "output-provider-secret", - }, - }, - type: "json", - value: { - reasoningEncryptedContent: "legitimate-tool-output", - }, - }, - providerOptions: { - openai: { - reasoningEncryptedContent: "result-provider-secret", - }, - }, - toolCallId: "call-1", - toolName: "lookup", - type: "tool-result", - }, - ], - providerOptions: { - openai: { reasoningEncryptedContent: "message-provider-secret" }, - }, - role: "assistant", - }, - ], - } as const; - - await emit({ - data: { - runtime: { - agentId: "agent-captured-input", - eveVersion: "0.22.1", - modelId: "dynamic:anthropic/claude-sonnet-5", - }, - }, - type: "session.started", - }); - await emit({ - data: { sequence: 0, turnId: "turn-captured-input" }, - type: "turn.started", - }); - instrumentation.events?.["step.started"]?.({ - modelInput, - session: { id: "session-captured-input" }, - step: { index: 0 }, - turn: { id: "turn-captured-input", sequence: 0 }, - }); - await emit({ - data: { sequence: 0, stepIndex: 0, turnId: "turn-captured-input" }, - type: "step.started", - }); - await emit({ - data: { - finishReason: "stop", - message: "Capture the model input.", - sequence: 0, - stepIndex: 0, - turnId: "turn-captured-input", - }, - type: "message.completed", - }); - await emit({ - data: { - finishReason: "stop", - sequence: 0, - stepIndex: 0, - turnId: "turn-captured-input", + it("records a turn with LLM and action spans", async () => { + const provider = braintrustEveInstrumentation({ + metadata: { + scenario: "eve-plugin-unit", + testRunId: "test-run-flat-tree", }, - type: "step.completed", - }); - await emit({ - data: { sequence: 0, turnId: "turn-captured-input" }, - type: "turn.completed", }); + const turnContext = providerContext(); + const firstModelContext = providerContext(); + const actionContext = providerContext(); + const secondModelContext = providerContext(); + const scope = ATTEMPT_SCOPE; - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - const step = spans.find( - (span) => span.span_attributes?.name === "eve.step", - ); - const session = spans.find( - (span) => span.span_attributes?.name === "eve.session", - ); - const turn = spans.find( - (span) => span.span_attributes?.name === "eve.turn", - ); - for (const span of [turn, step]) { - expect(span?.context?.span_origin).toMatchObject({ - instrumentation: { name: "eve" }, - }); - } - expect(step?.input).toEqual([ + await provider.events["turn.started"]( { - content: "Answer with the relevant Eve instrumentation detail.", - role: "system", + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", }, + turnContext, + ); + await provider.events["model.call.started"]( { - content: "Keep the answer concise.", - role: "system", + idempotencyKey: "model:attempt-0:0", + input: { + instructions: "You are a research agent.", + messages: [{ content: "Search then read", role: "user" }], + }, + model: { + modelId: "qwen/qwen3-30b-a3b", + provider: "openrouter.chat", + }, + scope, + type: "model.call.started", }, + firstModelContext, + ); + await provider.events["model.call.completed"]( { content: [ + { text: "I should search first.", type: "reasoning" }, { - text: "Readable reasoning", - type: "reasoning", - }, - { - input: { - providerOptions: { application: "tool-input" }, - reasoningEncryptedContent: "legitimate-tool-input", - }, - toolCallId: "call-1", - toolName: "lookup", + callId: "call-search", + input: { query: "Eve instrumentation" }, + toolName: "search", type: "tool-call", }, - { - output: { - type: "json", - value: { - reasoningEncryptedContent: "legitimate-tool-output", - }, - }, - toolCallId: "call-1", - toolName: "lookup", - type: "tool-result", - }, ], - role: "assistant", - }, - ]); - expect(JSON.stringify(step?.input)).not.toContain("provider-secret"); - expect(JSON.stringify(step?.input)).not.toContain("must-not-be-logged"); - expect(step?.metadata).toEqual({ - "eve.session_id": "session-captured-input", - }); - expect(session).toBeUndefined(); - expect(turn?.metadata).toEqual({ - "eve.session_id": "session-captured-input", - }); - expect(step?.metadata).not.toHaveProperty("model"); - expect(step?.metadata).not.toHaveProperty("provider"); - expect(turn?.metadata).not.toHaveProperty("model"); - expect(turn?.metadata).not.toHaveProperty("provider"); - expect(fakeEve.values.get("braintrust.eve.tracing")).toMatchObject({ - llmInputs: [], - }); - }); - - it("skips missing or malformed Eve instrumentation state without throwing", async () => { - const instrumentation = braintrustEveInstrumentation({ defineState }); - expect(() => - instrumentation.events?.["step.started"]?.({ bad: true } as never), - ).not.toThrow(); - - const fakeEve = createFakeDefineState(); - defineState = fakeEve.defineState; - fakeEve.values.set("braintrust.eve.tracing", { - llmInputs: [{ input: { content: "not an array" }, key: "bad" }], - }); - expect(() => - instrumentation.events?.["step.started"]?.({ - modelInput: { - messages: [{ content: "hello", role: "user" }], + finishReason: "tool-calls", + idempotencyKey: "model:attempt-0:0", + scope, + type: "model.call.completed", + usage: { + inputTokenDetails: { + cacheReadTokens: 3, + cacheWriteTokens: 2, + }, + inputTokens: 10, + outputTokens: 5, }, - session: { id: "session-malformed-state" }, - step: { index: 0 }, - turn: { id: "turn-malformed-state", sequence: 0 }, - }), - ).not.toThrow(); - - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - const ctx: EveHookContext = { session: { id: "session-no-state" } }; - await wildcard?.( - { - data: { sequence: 0, turnId: "turn-no-state" }, - type: "turn.started", }, - ctx, + firstModelContext, ); - await wildcard?.( + await provider.events["action.started"]( { - data: { sequence: 0, stepIndex: 0, turnId: "turn-no-state" }, - type: "step.started", + callId: "call-search", + idempotencyKey: "action:session-root:turn-0:call-search", + input: { query: "Eve instrumentation" }, + name: "search", + scope, + type: "action.started", }, - ctx, + actionContext, ); - await wildcard?.( + await provider.events["action.completed"]( { - data: { - finishReason: "stop", - sequence: 0, - stepIndex: 0, - turnId: "turn-no-state", + idempotencyKey: "action:session-root:turn-0:call-search", + output: { + output: { hits: ["eve.dev/docs"] }, + type: "result", }, - type: "step.completed", - }, - ctx, - ); - await wildcard?.( - { - data: { sequence: 0, turnId: "turn-no-state" }, - type: "turn.completed", + scope, + type: "action.completed", }, - ctx, - ); - - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - const step = spans.find( - (span) => span.span_attributes?.name === "eve.step", + actionContext, ); - expect(step?.input).toBeUndefined(); - }); - - it("bounds pre-existing durable trace state", async () => { - const fakeEve = createFakeDefineState(); - const oversizedEntryCount = 10_001; - fakeEve.values.set("braintrust.eve.tracing", { - llmInputs: [], - metadata: {}, - spanReferences: Array.from( - { length: oversizedEntryCount }, - (_, index) => ({ - exported: `exported-${index}`, - rootSpanId: `root-${index}`, - rowId: `row-${index}`, - spanId: `span-${index}`, - }), - ), - stepStarts: Array.from({ length: oversizedEntryCount }, (_, index) => ({ - open: false, - ordinal: index, - stepIndex: index, - turnId: `turn-${index}`, - })), - }); - const wildcard = braintrustEveHook({ - defineState: fakeEve.defineState, - }).events?.["*"]; - - await wildcard?.( + await provider.events["model.call.started"]( { - data: { - runtime: { - agentId: "agent-bounded-state", - eveVersion: "0.20.0", - modelId: "openai/gpt-5.4-mini", - }, + idempotencyKey: "model:attempt-1:0", + input: { + instructions: "You are a research agent.", + messages: [ + { content: "Search then read", role: "user" }, + { + content: [ + { + input: { query: "Eve instrumentation" }, + toolCallId: "call-search", + toolName: "search", + type: "tool-call", + }, + ], + role: "assistant", + }, + ], }, - type: "session.started", - }, - { session: { id: "session-bounded-state" } }, - ); - - const state = fakeEve.values.get("braintrust.eve.tracing") as { - spanReferences: unknown[]; - stepStarts: unknown[]; - }; - expect(state.spanReferences).toHaveLength(10_000); - expect(state.stepStarts).toHaveLength(10_000); - }); - - it("does not emit a span for session lifecycle metadata alone", async () => { - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - - await wildcard?.( - { - data: { - runtime: { - agentId: "agent-session-only", - eveVersion: "0.20.0", - modelId: "openai/gpt-5.4-mini", - }, + model: { + modelId: "qwen/qwen3-30b-a3b", + provider: "openrouter.chat", }, - type: "session.started", + scope, + type: "model.call.started", }, - { session: { id: "session-only" } }, + secondModelContext, ); - - expect(await backgroundLogger.drain()).toEqual([]); - }); - - it("records a flat Eve turn with session model metadata", async () => { - const wildcard = braintrustEveHook({ - defineState, - metadata: { - scenario: "eve-plugin-unit", - testRunId: "test-run-flat-tree", - }, - }).events?.["*"]; - expect(wildcard).toBeDefined(); - - const ctx: EveHookContext = { - session: { id: "session-flat-tree" }, - }; - const emit = (event: EveHandleMessageStreamEvent) => wildcard?.(event, ctx); - const expectedModelMetadata = { - model: "braintrust-eve-mock", - provider: "eve-mock", - }; - - await emit({ - data: { - runtime: { - agentId: "agent-id", - agentName: "eve-test-agent", - eveVersion: "0.20.0", - modelId: "eve-mock/braintrust-eve-mock", - }, - }, - meta: { at: "2026-01-01T00:00:00.000Z" }, - type: "session.started", - }); - await emit({ - data: { sequence: 0, turnId: "turn-flat-tree" }, - meta: { at: "2026-01-01T00:00:00.010Z" }, - type: "turn.started", - }); - await emit({ - data: { - message: "Search then read", - sequence: 0, - turnId: "turn-flat-tree", - }, - meta: { at: "2026-01-01T00:00:00.020Z" }, - type: "message.received", - }); - await emit({ - data: { sequence: 0, stepIndex: 0, turnId: "turn-flat-tree" }, - meta: { at: "2026-01-01T00:00:00.030Z" }, - type: "step.started", - }); - await emit({ - data: { - actions: [ + await provider.events["model.call.completed"]( + { + content: [ { - callId: "call-search", - input: { query: "Eve instrumentation" }, - kind: "tool-call", - toolName: "search", + text: "Here is the Eve instrumentation guide.", + type: "text", }, ], - sequence: 0, - stepIndex: 0, - turnId: "turn-flat-tree", - }, - meta: { at: "2026-01-01T00:00:00.040Z" }, - type: "actions.requested", - }); - await emit({ - data: { - error: undefined, - result: { - callId: "call-search", - kind: "tool-result", - output: { hits: ["eve.dev/docs"] }, - toolName: "search", - }, - sequence: 0, - status: "completed", - stepIndex: 0, - turnId: "turn-flat-tree", - }, - meta: { at: "2026-01-01T00:00:00.050Z" }, - type: "action.result", - }); - await emit({ - data: { - finishReason: "tool-calls", - sequence: 0, - stepIndex: 0, - turnId: "turn-flat-tree", - usage: { - cacheReadTokens: 3, - cacheWriteTokens: 2, - costUsd: 0.001, - inputTokens: 10, - outputTokens: 5, - }, - }, - meta: { at: "2026-01-01T00:00:00.060Z" }, - type: "step.completed", - }); - await emit({ - data: { sequence: 0, stepIndex: 1, turnId: "turn-flat-tree" }, - meta: { at: "2026-01-01T00:00:00.070Z" }, - type: "step.started", - }); - await emit({ - data: { - finishReason: "stop", - message: "Here is the Eve instrumentation guide.", - sequence: 0, - stepIndex: 1, - turnId: "turn-flat-tree", - }, - meta: { at: "2026-01-01T00:00:00.080Z" }, - type: "message.completed", - }); - await emit({ - data: { finishReason: "stop", - sequence: 0, - stepIndex: 1, - turnId: "turn-flat-tree", + idempotencyKey: "model:attempt-1:0", + scope, + type: "model.call.completed", usage: { inputTokens: 20, outputTokens: 8, }, }, - meta: { at: "2026-01-01T00:00:00.090Z" }, - type: "step.completed", - }); - await emit({ - data: { sequence: 0, turnId: "turn-flat-tree" }, - meta: { at: "2026-01-01T00:00:00.100Z" }, - type: "turn.completed", - }); + secondModelContext, + ); + await provider.events["turn.completed"]( + { + idempotencyKey: "turn:session-root:turn-0", + sessionId: "session-root", + turnId: "turn-0", + type: "turn.completed", + }, + turnContext, + ); const spans = (await backgroundLogger.drain()) as Array< Record @@ -598,8 +257,8 @@ describe("braintrustEveHook", () => { const root = spans.find( (span) => span.span_attributes?.name === "eve.turn", ); - const steps = spans.filter((span) => - String(span.span_attributes?.name).startsWith("eve.step"), + const steps = spans.filter( + (span) => span.span_attributes?.name === "eve.step", ); const tool = spans.find((span) => span.span_attributes?.name === "search"); @@ -612,78 +271,58 @@ describe("braintrustEveHook", () => { expect(root).toMatchObject({ input: [{ content: "Search then read", role: "user" }], metadata: { - ...expectedModelMetadata, - "eve.session_id": "session-flat-tree", + "eve.session_id": "session-root", scenario: "eve-plugin-unit", testRunId: "test-run-flat-tree", }, + output: "Here is the Eve instrumentation guide.", metrics: { completion_tokens: 13, - estimated_cost: 0.001, - prompt_cached_tokens: 3, prompt_cache_creation_tokens: 2, + prompt_cached_tokens: 3, prompt_tokens: 30, tokens: 43, }, - output: "Here is the Eve instrumentation guide.", span_attributes: { name: "eve.turn", type: "task", }, - root_span_id: deterministicEveIdForTest( - "eve:root", - "session-flat-tree", - "turn-flat-tree", - ), span_parents: [], }); + expect(root?.root_span_id).not.toBe(root?.span_id); + expect(root?.metadata).not.toHaveProperty("model"); + expect(root?.metadata).not.toHaveProperty("provider"); + expect(steps).toHaveLength(2); - expect(steps.map((span) => span.span_attributes?.name)).toEqual([ - "eve.step", - "eve.step", - ]); - expect(steps.map((span) => span.span_attributes?.type)).toEqual([ - "llm", - "llm", - ]); - expect(steps.map((span) => span.span_parents)).toEqual([ - [root?.span_id], - [root?.span_id], - ]); for (const step of steps) { - expect(step.metadata).toEqual({ - ...expectedModelMetadata, - "eve.session_id": "session-flat-tree", - scenario: "eve-plugin-unit", - testRunId: "test-run-flat-tree", + expect(step).toMatchObject({ + metadata: { + "eve.session_id": "session-root", + model: "qwen/qwen3-30b-a3b", + provider: "openrouter", + scenario: "eve-plugin-unit", + testRunId: "test-run-flat-tree", + }, + span_attributes: { + name: "eve.step", + type: "llm", + }, + span_parents: [root?.span_id], }); } - expect(steps[0]?.input).toBeUndefined(); - expect(steps[1]?.input).toBeUndefined(); - expect(tool).toMatchObject({ - input: { query: "Eve instrumentation" }, - metadata: { - "eve.session_id": "session-flat-tree", - scenario: "eve-plugin-unit", - testRunId: "test-run-flat-tree", - }, - output: { hits: ["eve.dev/docs"] }, - span_attributes: { - name: "search", - type: "tool", - }, - span_parents: [root?.span_id], - }); - expect(tool?.metadata).not.toHaveProperty("model"); - expect(tool?.metadata).not.toHaveProperty("provider"); + expect(steps[0]?.metadata).not.toHaveProperty("tools"); expect(steps[0]?.output).toMatchObject([ { finish_reason: "tool_calls", message: { + content: null, + reasoning: [{ content: "I should search first." }], tool_calls: [ { function: { - arguments: JSON.stringify({ query: "Eve instrumentation" }), + arguments: JSON.stringify({ + query: "Eve instrumentation", + }), name: "search", }, id: "call-search", @@ -702,1570 +341,1004 @@ describe("braintrustEveHook", () => { }, }, ]); + expect(tool).toMatchObject({ + input: { query: "Eve instrumentation" }, + metadata: { + "eve.session_id": "session-root", + scenario: "eve-plugin-unit", + testRunId: "test-run-flat-tree", + }, + output: { hits: ["eve.dev/docs"] }, + span_attributes: { + name: "search", + type: "tool", + }, + span_parents: [root?.span_id], + }); + expect(tool?.metadata).not.toHaveProperty("model"); + expect(tool?.metadata).not.toHaveProperty("provider"); }); - it("records each user message as a separate turn in one session", async () => { - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - expect(wildcard).toBeDefined(); - - const ctx: EveHookContext = { session: { id: "session-multi-turn" } }; - for (const [sequence, message] of [ - [0, "First user message"], - [1, "Second user message"], - ] as const) { - const turnId = `turn-${sequence}`; - await wildcard?.( - { data: { sequence, turnId }, type: "turn.started" }, - ctx, - ); - await wildcard?.( - { data: { message, sequence, turnId }, type: "message.received" }, - ctx, - ); - await wildcard?.( - { data: { sequence, turnId }, type: "turn.completed" }, - ctx, - ); - } - - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - const turns = spans.filter( - (span) => span.span_attributes?.name === "eve.turn", - ); + it("attaches a subagent turn beneath its action span", async () => { + const provider = braintrustEveInstrumentation(); + const parentTurnContext = providerContext(); + const actionContext = providerContext(); + const childTurnContext = providerContext(); + const childModelContext = providerContext(); + const parentScope = ATTEMPT_SCOPE; - expect( - spans.some((span) => span.span_attributes?.name === "eve.session"), - ).toBe(false); - expect(turns).toHaveLength(2); - expect(turns.map((turn) => turn.span_parents)).toEqual([[], []]); - expect(turns.map((turn) => turn.root_span_id)).toEqual([ - deterministicEveIdForTest("eve:root", "session-multi-turn", "turn-0"), - deterministicEveIdForTest("eve:root", "session-multi-turn", "turn-1"), - ]); - expect(turns[0]?.root_span_id).not.toBe(turns[1]?.root_span_id); - expect(turns.map((turn) => turn.input)).toEqual([ - [{ content: "First user message", role: "user" }], - [{ content: "Second user message", role: "user" }], - ]); - }); - - it("merges incremental tool-call batches without reconstructing later LLM inputs", async () => { - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - expect(wildcard).toBeDefined(); - - const ctx: EveHookContext = { - session: { id: "session-incremental-tools" }, - }; - const emit = (event: EveHandleMessageStreamEvent) => wildcard?.(event, ctx); - - await emit({ - data: { sequence: 0, turnId: "turn-incremental-tools" }, - type: "turn.started", - }); - await emit({ - data: { - message: "Search then read", - sequence: 0, - turnId: "turn-incremental-tools", - }, - type: "message.received", - }); - await emit({ - data: { sequence: 0, stepIndex: 0, turnId: "turn-incremental-tools" }, - type: "step.started", - }); - await emit({ - data: { - actions: [ - { - callId: "call-search", - input: { query: "Eve instrumentation" }, - kind: "tool-call", - toolName: "search", - }, - ], + await provider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", sequence: 0, - stepIndex: 0, - turnId: "turn-incremental-tools", + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", }, - type: "actions.requested", - }); - await emit({ - data: { - actions: [ - { - callId: "call-search", - input: { query: "Updated Eve instrumentation" }, - kind: "tool-call", - toolName: "search", - }, - ], - sequence: 0, - stepIndex: 0, - turnId: "turn-incremental-tools", + parentTurnContext, + ); + await provider.events["action.started"]( + { + callId: "call-researcher", + idempotencyKey: "action:session-root:turn-0:call-researcher", + input: { message: "Research Eve" }, + name: "researcher", + scope: parentScope, + type: "action.started", }, - type: "actions.requested", - }); - await emit({ - data: { - actions: [ - { - callId: "call-read", - input: { url: "https://eve.dev/docs/guides/instrumentation" }, - kind: "tool-call", - toolName: "read", + actionContext, + ); + await Promise.all([ + provider.events["turn.started"]( + { + idempotencyKey: "turn:session-child:turn-0", + parentLineage: { + callId: "call-researcher", + sessionId: "session-root", + turnId: "turn-0", }, - ], - sequence: 0, - stepIndex: 0, - turnId: "turn-incremental-tools", - }, - type: "actions.requested", - }); - await emit({ - data: { - result: { - callId: "call-search", - kind: "tool-result", - output: { url: "https://eve.dev/docs/guides/instrumentation" }, - toolName: "search", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-child", + turnId: "turn-0", + type: "turn.started", }, - sequence: 0, - status: "completed", - stepIndex: 0, - turnId: "turn-incremental-tools", - }, - type: "action.result", - }); - await emit({ - data: { - result: { - callId: "call-read", - kind: "tool-result", - output: { excerpt: "Eve hooks expose runtime stream events." }, - toolName: "read", + childTurnContext, + ), + provider.events["model.call.started"]( + { + idempotencyKey: "model:child-attempt-0:0", + model: { + modelId: "qwen/qwen3-30b-a3b", + provider: "openrouter.chat", + }, + scope: { + attemptId: "child-attempt-0", + attemptIndex: 0, + rootSessionId: "session-root", + sessionId: "session-child", + stepIndex: 0, + turnId: "turn-0", + }, + type: "model.call.started", }, - sequence: 0, - status: "completed", - stepIndex: 0, - turnId: "turn-incremental-tools", - }, - type: "action.result", - }); - await emit({ - data: { - finishReason: "tool-calls", - sequence: 0, - stepIndex: 0, - turnId: "turn-incremental-tools", - }, - type: "step.completed", - }); - await emit({ - data: { sequence: 0, stepIndex: 1, turnId: "turn-incremental-tools" }, - type: "step.started", - }); - await emit({ - data: { - finishReason: "stop", - message: "Done.", - sequence: 0, - stepIndex: 1, - turnId: "turn-incremental-tools", - }, - type: "message.completed", - }); - await emit({ - data: { - finishReason: "stop", - sequence: 0, - stepIndex: 1, - turnId: "turn-incremental-tools", - }, - type: "step.completed", - }); - await emit({ - data: { sequence: 0, turnId: "turn-incremental-tools" }, - type: "turn.completed", - }); + childModelContext, + ), + ]); const spans = (await backgroundLogger.drain()) as Array< Record >; - const steps = spans.filter((span) => - String(span.span_attributes?.name).startsWith("eve.step"), + const parent = spans.find( + (span) => + span.span_attributes?.name === "eve.turn" && + span.metadata?.["eve.session_id"] === "session-root", + ); + const action = spans.find( + (span) => span.span_attributes?.name === "researcher", + ); + const child = spans.find( + (span) => + span.span_attributes?.name === "eve.turn" && + span.metadata?.["eve.session_id"] === "session-child", + ); + const childModel = spans.find( + (span) => span.span_attributes?.name === "eve.step", ); - expect(steps[0]?.output).toMatchObject([ - { - finish_reason: "tool_calls", - message: { - tool_calls: [ - { - function: { - arguments: JSON.stringify({ - query: "Updated Eve instrumentation", - }), - name: "search", - }, - id: "call-search", - type: "function", - }, - { - function: { - arguments: JSON.stringify({ - url: "https://eve.dev/docs/guides/instrumentation", - }), - name: "read", - }, - id: "call-read", - type: "function", - }, - ], - }, - }, - ]); - expect(steps[1]?.input).toBeUndefined(); + expect(action?.span_parents).toEqual([parent?.span_id]); + expect(child?.span_parents).toEqual([action?.span_id]); + expect(child?.root_span_id).toBe(parent?.root_span_id); + expect(childModel?.span_parents).toEqual([child?.span_id]); + expect(childModel?.root_span_id).toBe(parent?.root_span_id); }); - it("preserves reasoning across tool, text, and structured outputs", async () => { - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - const ctx: EveHookContext = { - session: { id: "session-reasoning-outputs" }, + it("keeps a stable root across nested subagents with different turn ids", async () => { + const provider = braintrustEveInstrumentation(); + const rootContext = providerContext(); + const rootScope = { ...ATTEMPT_SCOPE, turnId: "root-turn" }; + const childScope: EveInstrumentationAttemptScope = { + attemptId: "child-attempt", + attemptIndex: 0, + rootSessionId: "session-root", + sessionId: "session-child", + stepIndex: 0, + turnId: "child-turn", }; - const emit = (event: EveHandleMessageStreamEvent) => wildcard?.(event, ctx); - const turnId = "turn-reasoning-outputs"; - - await emit({ data: { sequence: 0, turnId }, type: "turn.started" }); - await emit({ - data: { sequence: 0, stepIndex: 0, turnId }, - type: "step.started", - }); - await emit({ - data: { - reasoning: "I should inspect the available tools.", - sequence: 0, - stepIndex: 0, - turnId, - }, - meta: { at: "2026-01-01T00:00:00.010Z" }, - type: "reasoning.completed", - }); - await emit({ - data: { - finishReason: "tool-calls", - message: null, - sequence: 0, - stepIndex: 0, - turnId, - }, - type: "message.completed", - }); - await emit({ - data: { - actions: [ - { - callId: "call-reasoning-search", - input: { query: "Eve reasoning" }, - kind: "tool-call", - toolName: "search", - }, - ], - sequence: 0, - stepIndex: 0, - turnId, - }, - type: "actions.requested", - }); - await emit({ - data: { finishReason: "tool-calls", sequence: 0, stepIndex: 0, turnId }, - type: "step.completed", - }); - - await emit({ - data: { sequence: 0, stepIndex: 1, turnId }, - type: "step.started", - }); - await emit({ - data: { - reasoning: "The tool returned the relevant guide.", - sequence: 0, - stepIndex: 1, - turnId, - }, - meta: { at: "2026-01-01T00:00:00.020Z" }, - type: "reasoning.completed", - }); - await emit({ - data: { - reasoning: "I can now answer concisely.", - sequence: 0, - stepIndex: 1, - turnId, - }, - meta: { at: "2026-01-01T00:00:00.030Z" }, - type: "reasoning.completed", - }); - await emit({ - data: { - finishReason: "stop", - message: "Eve exposes completed reasoning events.", - sequence: 0, - stepIndex: 1, - turnId, - }, - type: "message.completed", - }); - await emit({ - data: { finishReason: "stop", sequence: 0, stepIndex: 1, turnId }, - type: "step.completed", - }); - - await emit({ - data: { sequence: 0, stepIndex: 2, turnId }, - type: "step.started", - }); - await emit({ - data: { - reasoning: "The response must match the requested schema.", - sequence: 0, - stepIndex: 2, - turnId, + await provider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:root-turn", + rootSessionId: "session-root", + sequence: 4, + sessionId: "session-root", + turnId: "root-turn", + type: "turn.started", }, - meta: { at: "2026-01-01T00:00:00.040Z" }, - type: "reasoning.completed", - }); - await emit({ - data: { - result: { supported: true }, - sequence: 0, - stepIndex: 2, - turnId, + rootContext, + ); + await provider.events["action.started"]( + { + callId: "call-child", + idempotencyKey: "action:session-root:root-turn:call-child", + name: "child", + scope: rootScope, + type: "action.started", }, - type: "result.completed", - }); - await emit({ - data: { finishReason: "stop", sequence: 0, stepIndex: 2, turnId }, - type: "step.completed", - }); - await emit({ data: { sequence: 0, turnId }, type: "turn.completed" }); - - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - const steps = spans.filter( - (span) => span.span_attributes?.name === "eve.step", + providerContext(), ); - expect(steps).toHaveLength(3); - expect(steps[0]?.output).toMatchObject([ + await provider.events["turn.started"]( { - finish_reason: "tool_calls", - message: { - content: null, - reasoning: [{ content: "I should inspect the available tools." }], - tool_calls: [ - { - function: { name: "search" }, - id: "call-reasoning-search", - type: "function", - }, - ], + idempotencyKey: "turn:session-child:child-turn", + parentLineage: { + callId: "call-child", + sessionId: "session-root", + turnId: "root-turn", }, + rootSessionId: "session-root", + sequence: 2, + sessionId: "session-child", + turnId: "child-turn", + type: "turn.started", }, - ]); - expect(steps[1]?.output).toMatchObject([ + providerContext(), + ); + await provider.events["action.started"]( { - finish_reason: "stop", - message: { - content: "Eve exposes completed reasoning events.", - reasoning: [ - { content: "The tool returned the relevant guide." }, - { content: "I can now answer concisely." }, - ], - }, + callId: "call-grandchild", + idempotencyKey: "action:session-child:child-turn:call-grandchild", + name: "grandchild", + scope: childScope, + type: "action.started", }, - ]); - expect(steps[2]?.output).toMatchObject([ + providerContext(), + ); + const rootReference = rootContext.state.get(); + if ( + typeof rootReference !== "object" || + rootReference === null || + Array.isArray(rootReference) || + typeof rootReference.rootSpanId !== "string" + ) { + throw new Error("Expected the root turn trace context to be persisted"); + } + const resumedProvider = braintrustEveInstrumentation(); + await resumedProvider.events["turn.started"]( { - finish_reason: "stop", - message: { - content: { supported: true }, - reasoning: [ - { content: "The response must match the requested schema." }, - ], + idempotencyKey: "turn:session-grandchild:grandchild-turn", + parentLineage: { + callId: "call-grandchild", + sessionId: "session-child", + turnId: "child-turn", + }, + parentTraceContext: { + spanId: "1111111111111111", + traceFlags: 1, + traceId: rootReference.rootSpanId, }, + rootSessionId: "session-root", + sequence: 7, + sessionId: "session-grandchild", + turnId: "grandchild-turn", + type: "turn.started", }, + providerContext(), + ); + + const spans = (await backgroundLogger.drain()) as Array< + Record + >; + const turns = spans.filter( + (span) => span.span_attributes?.name === "eve.turn", + ); + const root = turns.find( + (turn) => turn.metadata?.["eve.session_id"] === "session-root", + ); + const child = turns.find( + (turn) => turn.metadata?.["eve.session_id"] === "session-child", + ); + const grandchild = turns.find( + (turn) => turn.metadata?.["eve.session_id"] === "session-grandchild", + ); + const childAction = spans.find( + (span) => span.span_attributes?.name === "child", + ); + const grandchildAction = spans.find( + (span) => span.span_attributes?.name === "grandchild", + ); + + expect(child?.span_parents).toEqual([childAction?.span_id]); + expect(grandchild?.span_parents).toEqual([grandchildAction?.span_id]); + expect(turns.map((turn) => turn.root_span_id)).toEqual([ + root?.root_span_id, + root?.root_span_id, + root?.root_span_id, ]); }); - it("rehydrates reasoning idempotently across Vercel Workflow steps", async () => { - const eveState = createFakeDefineState(); - const ctx: EveHookContext = { - session: { id: "session-reasoning-replay" }, - }; - const turnId = "turn-reasoning-replay"; - const reasoningEvent = { - data: { - reasoning: "Use the search tool before answering.", - sequence: 0, - stepIndex: 0, - turnId, - }, - meta: { at: "2026-01-01T00:00:00.020Z" }, - type: "reasoning.completed", - } as const satisfies EveHandleMessageStreamEvent; - const initialHook = braintrustEveHook({ - defineState: eveState.defineState, - }).events?.["*"]; + it("creates a separate trace for each top-level turn in one session", async () => { + const provider = braintrustEveInstrumentation(); - await initialHook?.( + await provider.events["turn.started"]( { - data: { sequence: 0, turnId }, - meta: { at: "2026-01-01T00:00:00.000Z" }, + idempotencyKey: "turn:shared-session:turn-0", + rootSessionId: "shared-session", + sequence: 0, + sessionId: "shared-session", + turnId: "turn-0", type: "turn.started", }, - ctx, + providerContext(), ); - await initialHook?.( + await provider.events["turn.started"]( { - data: { sequence: 0, stepIndex: 0, turnId }, - meta: { at: "2026-01-01T00:00:00.010Z" }, - type: "step.started", + idempotencyKey: "turn:shared-session:turn-1", + rootSessionId: "shared-session", + sequence: 1, + sessionId: "shared-session", + turnId: "turn-1", + type: "turn.started", }, - ctx, + providerContext(), ); - await initialHook?.(reasoningEvent, ctx); - const initialWrites = (await backgroundLogger.drain()) as Array< - Record & { id: string } + const turns = (await backgroundLogger.drain()) as Array< + Record >; - expect( - initialWrites - .filter((span) => span.span_attributes?.name) - .every( - (span) => span.context?.span_origin?.instrumentation?.name === "eve", - ), - ).toBe(true); - for (const [key, value] of eveState.values) { - eveState.values.set(key, JSON.parse(JSON.stringify(value))); - } + expect(turns).toHaveLength(2); + expect(turns.map((turn) => turn.metadata?.["eve.session_id"])).toEqual([ + "shared-session", + "shared-session", + ]); + expect(new Set(turns.map((turn) => turn.root_span_id)).size).toBe(2); + }); + + it("preserves a durable subagent turn across provider instances", async () => { + const firstProvider = braintrustEveInstrumentation(); + const parentTurnContext = providerContext(); + const childTurnContext = providerContext(); - const resumedHook = braintrustEveHook({ - defineState: eveState.defineState, - }).events?.["*"]; - await resumedHook?.( + await firstProvider.events["turn.started"]( { - data: { sequence: 0, turnId }, - meta: { at: "2026-01-01T00:00:00.000Z" }, + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-root", + turnId: "turn-0", type: "turn.started", }, - ctx, + parentTurnContext, ); - await resumedHook?.( + await firstProvider.events["action.started"]( { - data: { sequence: 0, stepIndex: 0, turnId }, - meta: { at: "2026-01-01T00:00:00.010Z" }, - type: "step.started", + callId: "call-researcher", + idempotencyKey: "action:session-root:turn-0:call-researcher", + name: "researcher", + scope: ATTEMPT_SCOPE, + type: "action.started", }, - ctx, + providerContext(), ); - const resumedStartWrites = (await backgroundLogger.drain()) as Array< - Record & { id: string } - >; - await resumedHook?.(reasoningEvent, ctx); - await resumedHook?.( + await firstProvider.events["turn.started"]( { - data: { - finishReason: "tool-calls", - message: null, - sequence: 0, - stepIndex: 0, - turnId, + idempotencyKey: "turn:session-child:turn-0", + parentLineage: { + callId: "call-researcher", + sessionId: "session-root", + turnId: "turn-0", }, - type: "message.completed", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-child", + turnId: "turn-0", + type: "turn.started", }, - ctx, + childTurnContext, ); - await resumedHook?.( + + const resumedProvider = braintrustEveInstrumentation(); + const childScope: EveInstrumentationAttemptScope = { + attemptId: "child-attempt-0", + attemptIndex: 0, + rootSessionId: "session-root", + sessionId: "session-child", + stepIndex: 0, + turnId: "turn-0", + }; + const modelContext = providerContext(); + await resumedProvider.events["model.call.started"]( { - data: { - actions: [ - { - callId: "call-replayed-search", - input: { query: "Vercel Workflow reasoning" }, - kind: "tool-call", - toolName: "search", - }, - ], - sequence: 0, - stepIndex: 0, - turnId, + idempotencyKey: "model:child-attempt-0:0", + input: { + messages: [{ content: "Continue durably", role: "user" }], }, - type: "actions.requested", + model: { modelId: "gpt-5.4-mini", provider: "openai.responses" }, + scope: childScope, + type: "model.call.started", }, - ctx, + modelContext, ); - const resumedIntermediateWrites = (await backgroundLogger.drain()) as Array< - Record & { id: string } - >; - await resumedHook?.( + await resumedProvider.events["model.call.completed"]( { - data: { finishReason: "tool-calls", sequence: 0, stepIndex: 0, turnId }, - type: "step.completed", + content: [{ text: "Durable answer", type: "text" }], + finishReason: "stop", + idempotencyKey: "model:child-attempt-0:0", + scope: childScope, + type: "model.call.completed", + usage: { inputTokens: 3, outputTokens: 2 }, }, - ctx, + modelContext, ); - await resumedHook?.( - { data: { sequence: 0, turnId }, type: "turn.completed" }, - ctx, + await resumedProvider.events["turn.completed"]( + { + idempotencyKey: "turn:session-child:turn-0", + sessionId: "session-child", + turnId: "turn-0", + type: "turn.completed", + }, + providerContext(childTurnContext.state.get()), ); - const resumedWrites = (await backgroundLogger.drain()) as Array< - Record & { id: string } + const spans = (await backgroundLogger.drain()) as Array< + Record >; - const allWrites = [ - ...initialWrites, - ...resumedStartWrites, - ...resumedIntermediateWrites, - ...resumedWrites, - ]; - const stepRowId = deterministicEveIdForTest( - "eve:row:step", - "session-reasoning-replay", - turnId, - "0", + const parent = spans.find( + (span) => + span.span_attributes?.name === "eve.turn" && + span.metadata?.["eve.session_id"] === "session-root", ); - expect( - allWrites.filter( - (write) => write.id === stepRowId && write.output !== undefined, - ), - ).toHaveLength(1); - - const spans = mergeRowBatch([...allWrites].reverse()); - const steps = spans.filter( - (span) => span.span_attributes?.name === "eve.step", + const action = spans.find( + (span) => span.span_attributes?.name === "researcher", ); - expect(steps).toHaveLength(1); - expect(steps[0]?.span_id).toBe( - deterministicEveIdForTest( - "eve:step", - "session-reasoning-replay", - turnId, - "0", - ), + const child = spans.find( + (span) => + span.span_attributes?.name === "eve.turn" && + span.metadata?.["eve.session_id"] === "session-child", ); - expect(steps[0]?.output?.[0]?.message).toMatchObject({ - content: null, - reasoning: [{ content: "Use the search tool before answering." }], - tool_calls: [ - { - function: { name: "search" }, - id: "call-replayed-search", - type: "function", - }, - ], - }); - expect(steps[0]?.output?.[0]?.message?.reasoning).toHaveLength(1); - expect(eveState.values.get("braintrust.eve.tracing")).toMatchObject({ - reasoningBlocks: [], - stepStarts: [], + const step = spans.find( + (span) => span.span_attributes?.name === "eve.step", + ); + expect(child).toMatchObject({ + input: [{ content: "Continue durably", role: "user" }], + metrics: { + completion_tokens: 2, + prompt_tokens: 3, + tokens: 5, + }, + output: "Durable answer", + span_parents: [action?.span_id], }); + expect(child?.root_span_id).toBe(parent?.root_span_id); + expect(step?.span_parents).toEqual([child?.span_id]); + expect(step?.root_span_id).toBe(parent?.root_span_id); }); - it("merges late tool results into tool spans closed by turn completion", async () => { - const eveState = createFakeDefineState(); - const wildcard = braintrustEveHook({ - defineState: eveState.defineState, - metadata: { - scenario: "eve-plugin-unit", - testRunId: "test-run-late-tool-result", - }, - }).events?.["*"]; - expect(wildcard).toBeDefined(); + it("resumes spans and turn metrics from Eve operation state", async () => { + const firstProvider = braintrustEveInstrumentation(); + const turnContext = providerContext(); + const modelContext = providerContext(); + const scope = ATTEMPT_SCOPE; - const ctx: EveHookContext = { - session: { id: "session-late-tool-result" }, - }; - const emit = (event: EveHandleMessageStreamEvent) => wildcard?.(event, ctx); - - await emit({ - data: { sequence: 0, turnId: "turn-late-tool-result" }, - meta: { at: "2026-01-01T00:00:00.000Z" }, - type: "turn.started", - }); - await emit({ - data: { sequence: 0, stepIndex: 0, turnId: "turn-late-tool-result" }, - meta: { at: "2026-01-01T00:00:00.010Z" }, - type: "step.started", - }); - await emit({ - data: { - actions: [ - { - callId: "call-late-search", - input: { query: "late tool result" }, - kind: "tool-call", - toolName: "search", - }, - ], + await firstProvider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", sequence: 0, - stepIndex: 0, - turnId: "turn-late-tool-result", - }, - meta: { at: "2026-01-01T00:00:00.020Z" }, - type: "actions.requested", - }); - await emit({ - data: { sequence: 0, turnId: "turn-late-tool-result" }, - meta: { at: "2026-01-01T00:00:00.030Z" }, - type: "turn.completed", - }); - expect(eveState.values.get("braintrust.eve.tracing")).toMatchObject({ - spanReferences: expect.arrayContaining([ - expect.objectContaining({ - exported: expect.any(String), - rootSpanId: deterministicEveIdForTest( - "eve:root", - "session-late-tool-result", - "turn-late-tool-result", - ), - rowId: deterministicEveIdForTest( - "eve:row:tool", - "session-late-tool-result", - "turn-late-tool-result", - "call-late-search", - ), - spanId: deterministicEveIdForTest( - "eve:tool", - "session-late-tool-result", - "turn-late-tool-result", - "call-late-search", - ), - }), - ]), - stepStarts: [], - }); - const initialWrites = (await backgroundLogger.drain()) as Array< - Record & { id: string } - >; - const resumedWildcard = braintrustEveHook({ - defineState: eveState.defineState, - metadata: { - scenario: "eve-plugin-unit", - testRunId: "test-run-late-tool-result", + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", }, - }).events?.["*"]; - const flushSpy = vi.spyOn(backgroundLogger, "flush"); - flushSpy.mockClear(); - await resumedWildcard?.( + turnContext, + ); + await firstProvider.events["model.call.started"]( { - data: { - result: { - callId: "call-late-search", - kind: "tool-result", - output: { title: "Late result" }, - toolName: "search", - }, - sequence: 0, - status: "completed", - stepIndex: 0, - turnId: "turn-late-tool-result", + idempotencyKey: "model:attempt-0:0", + input: { + messages: [{ content: "Resume this call", role: "user" }], + }, + model: { + modelId: "gpt-5.4-mini", + provider: "openai.responses", }, - meta: { at: "2026-01-01T00:00:00.040Z" }, - type: "action.result", + scope, + type: "model.call.started", }, - ctx, - ); - expect(flushSpy).not.toHaveBeenCalled(); - - const resumedWrites = (await backgroundLogger.drain()) as Array< - Record & { id: string } - >; - expect( - [...initialWrites, ...resumedWrites].every( - (span) => span._is_merge === true, - ), - ).toBe(true); - - // The backend may ingest separate workflow uploads out of order. Because - // every write is a merge, a delayed initial write cannot erase the result. - const spans = mergeRowBatch([...resumedWrites, ...initialWrites]); - const turns = spans.filter( - (span) => span.span_attributes?.name === "eve.turn", + modelContext, ); - const tool = spans.find((span) => span.span_attributes?.name === "search"); - expect(turns).toHaveLength(1); - expect( - spans.filter((span) => span.span_attributes?.name === "search"), - ).toHaveLength(1); - expect(tool).toMatchObject({ - input: { query: "late tool result" }, - metadata: { - scenario: "eve-plugin-unit", - testRunId: "test-run-late-tool-result", - }, - output: { title: "Late result" }, - span_attributes: { - name: "search", - type: "tool", - }, - span_parents: [turns[0]?.span_id], + const persisted = modelContext.state.get(); + expect(persisted).toMatchObject({ + exported: expect.any(String), + rootSpanId: expect.any(String), + spanId: expect.any(String), }); - expect(tool?.metrics?.end).toEqual(expect.any(Number)); - }); - it("lets action results complete sparse subagent events across workflow steps", async () => { - const eveState = createFakeDefineState(); - const ctx: EveHookContext = { - session: { id: "session-sparse-subagent" }, - }; - const firstWildcard = braintrustEveHook({ - defineState: eveState.defineState, - }).events?.["*"]; - - await firstWildcard?.( + const resumedProvider = braintrustEveInstrumentation(); + await resumedProvider.events["model.call.completed"]( { - data: { sequence: 0, turnId: "turn-sparse-subagent" }, - type: "turn.started", + content: [{ text: "Resumed output", type: "text" }], + finishReason: "stop", + idempotencyKey: "model:attempt-0:0", + scope, + type: "model.call.completed", + usage: { inputTokens: 4, outputTokens: 2 }, }, - ctx, + providerContext(persisted), ); - await firstWildcard?.( + const secondModelContext = providerContext(); + await resumedProvider.events["model.call.started"]( { - data: { - actions: [ - { - callId: "call-sparse-subagent", - input: { message: "Research Eve" }, - kind: "subagent-call", - subagentName: "researcher", - }, - ], - sequence: 0, - stepIndex: 0, - turnId: "turn-sparse-subagent", + idempotencyKey: "model:attempt-0:1", + input: { + messages: [{ content: "Continue this call", role: "user" }], + }, + model: { + modelId: "gpt-5.4-mini", + provider: "openai.responses", }, - type: "actions.requested", + scope: { ...scope, stepIndex: 1 }, + type: "model.call.started", }, - ctx, + secondModelContext, ); - await firstWildcard?.( - { - data: { - callId: "call-sparse-subagent", - sequence: 0, - subagentName: "researcher", - turnId: "turn-sparse-subagent", + const secondPersisted = secondModelContext.state.get(); + expect(secondPersisted).toMatchObject({ + turnMetricContributions: { + "model:attempt-0:0": { + completion_tokens: 2, + prompt_tokens: 4, + tokens: 6, }, - meta: { at: "2026-01-01T00:00:00.100Z" }, - type: "subagent.completed", }, - ctx, - ); - - const resumedWildcard = braintrustEveHook({ - defineState: eveState.defineState, - }).events?.["*"]; - await resumedWildcard?.( + }); + const finalProvider = braintrustEveInstrumentation(); + await finalProvider.events["model.call.completed"]( { - data: { - result: { - callId: "call-sparse-subagent", - kind: "subagent-result", - output: { answer: "Authoritative result" }, - subagentName: "researcher", - }, - sequence: 0, - status: "completed", - stepIndex: 0, - turnId: "turn-sparse-subagent", - }, - meta: { at: "2026-01-01T00:00:00.500Z" }, - type: "action.result", + content: [{ text: "Final resumed output", type: "text" }], + finishReason: "stop", + idempotencyKey: "model:attempt-0:1", + scope: { ...scope, stepIndex: 1 }, + type: "model.call.completed", + usage: { inputTokens: 3, outputTokens: 1 }, }, - ctx, + providerContext(secondPersisted), ); const spans = (await backgroundLogger.drain()) as Array< Record >; - const subagents = spans.filter( - (span) => span.span_attributes?.name === "researcher", + const steps = spans.filter( + (span) => span.span_attributes?.name === "eve.step", ); - expect(subagents).toHaveLength(1); - expect(subagents[0]).toMatchObject({ - output: { answer: "Authoritative result" }, - metrics: { end: Date.parse("2026-01-01T00:00:00.100Z") / 1000 }, - span_attributes: { type: "tool" }, - }); - }); - - it("evicts tracing state after session completion", async () => { - const fakeEve = createFakeDefineState(); - defineState = fakeEve.defineState; - const wildcard = braintrustEveHook({ - defineState, - metadata: { - scenario: "eve-plugin-unit", - testRunId: "test-run-late-after-session", + expect(steps).toHaveLength(2); + expect(steps[0]).toMatchObject({ + input: [{ content: "Resume this call", role: "user" }], + metrics: { + completion_tokens: 2, + prompt_tokens: 4, + tokens: 6, }, - }).events?.["*"]; - expect(wildcard).toBeDefined(); - - const ctx: EveHookContext = { - session: { id: "session-late-after-session" }, - }; - const emit = (event: EveHandleMessageStreamEvent) => wildcard?.(event, ctx); - - await emit({ - data: { sequence: 0, turnId: "turn-late-after-session" }, - meta: { at: "2026-01-01T00:00:00.000Z" }, - type: "turn.started", - }); - await emit({ - data: { sequence: 0, stepIndex: 0, turnId: "turn-late-after-session" }, - meta: { at: "2026-01-01T00:00:00.010Z" }, - type: "step.started", - }); - await emit({ - data: { - actions: [ - { - callId: "call-after-session", - input: { query: "after session" }, - kind: "tool-call", - toolName: "search", + output: [ + { + finish_reason: "stop", + message: { + content: "Resumed output", + role: "assistant", }, - ], - sequence: 0, - stepIndex: 0, - turnId: "turn-late-after-session", - }, - meta: { at: "2026-01-01T00:00:00.020Z" }, - type: "actions.requested", - }); - await emit({ - meta: { at: "2026-01-01T00:00:00.030Z" }, - type: "session.completed", - }); - - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - const tool = spans.find((span) => span.span_attributes?.name === "search"); - expect(tool).toMatchObject({ - input: { query: "after session" }, - }); - expect(tool?.metrics?.end).toEqual(expect.any(Number)); - expect(fakeEve.values.get("braintrust.eve.tracing")).toEqual({ - llmInputs: [], - metadata: {}, - reasoningBlocks: [], - spanReferences: [], - stepStarts: [], - }); - }); - - it("records result-only tool events without invented input", async () => { - const wildcard = braintrustEveHook({ - defineState, - metadata: { - scenario: "eve-plugin-unit", - testRunId: "test-run-result-only", - }, - }).events?.["*"]; - expect(wildcard).toBeDefined(); - - const ctx: EveHookContext = { - session: { id: "session-result-only" }, - }; - const emit = (event: EveHandleMessageStreamEvent) => wildcard?.(event, ctx); - - await emit({ - data: { - result: { - callId: "call-result-only", - kind: "tool-result", - output: { title: "Result only" }, - toolName: "search", }, - sequence: 0, - status: "completed", - stepIndex: 0, - turnId: "turn-result-only", - }, - type: "action.result", - }); - await emit({ - data: { sequence: 0, turnId: "turn-result-only" }, - type: "turn.completed", + ], }); - - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - const tool = spans.find((span) => span.span_attributes?.name === "search"); - expect(tool).toMatchObject({ - output: { title: "Result only" }, - span_attributes: { name: "search", type: "tool" }, + const turn = spans.find( + (span) => span.span_attributes?.name === "eve.turn", + ); + expect(turn?.metrics).toMatchObject({ + completion_tokens: 3, + prompt_tokens: 7, + tokens: 10, }); - expect(tool?.input).toBeUndefined(); }); - it("evicts tracing state after session failure", async () => { - const fakeEve = createFakeDefineState(); - const wildcard = braintrustEveHook({ - defineState: fakeEve.defineState, - }).events?.["*"]; - const ctx: EveHookContext = { - session: { id: "session-failed-cleanup" }, - }; - - await wildcard?.( + it("does not double-count replayed model completions", async () => { + const provider = braintrustEveInstrumentation(); + const modelContext = providerContext(); + await provider.events["turn.started"]( { - data: { sequence: 0, turnId: "turn-failed-cleanup" }, + idempotencyKey: "turn:replay-session:turn-0", + rootSessionId: "replay-session", + sequence: 0, + sessionId: "replay-session", + turnId: "turn-0", type: "turn.started", }, - ctx, - ); - await wildcard?.( - { - data: { - sequence: 0, - stepIndex: 0, - turnId: "turn-failed-cleanup", - }, - type: "step.started", - }, - ctx, + providerContext(), ); - await wildcard?.( + const scope = { + ...ATTEMPT_SCOPE, + attemptId: "replay-attempt", + rootSessionId: "replay-session", + sessionId: "replay-session", + }; + await provider.events["model.call.started"]( { - data: { - code: "session_failed", - message: "Session failed", - sessionId: "session-failed-cleanup", - }, - type: "session.failed", + idempotencyKey: "model:replay-attempt:0", + model: { modelId: "gpt-5.4-mini", provider: "openai.responses" }, + scope, + type: "model.call.started", }, - ctx, + modelContext, ); + const completed: EveInstrumentationModelCallCompletedEvent = { + content: [{ text: "Done", type: "text" }], + finishReason: "stop", + idempotencyKey: "model:replay-attempt:0", + scope, + type: "model.call.completed", + usage: { inputTokens: 3, outputTokens: 2 }, + }; + await provider.events["model.call.completed"](completed, modelContext); + await provider.events["model.call.completed"](completed, modelContext); - expect(fakeEve.values.get("braintrust.eve.tracing")).toEqual({ - llmInputs: [], - metadata: {}, - reasoningBlocks: [], - spanReferences: [], - stepStarts: [], + const spans = (await backgroundLogger.drain()) as Array< + Record + >; + const turn = spans.find( + (span) => span.span_attributes?.name === "eve.turn", + ); + expect(turn?.metrics).toMatchObject({ + completion_tokens: 2, + prompt_tokens: 3, + tokens: 5, }); }); - it("flushes final session events but not ordinary or ignored events", async () => { - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - const ctx: EveHookContext = { - session: { id: "session-selective-flush" }, - }; + it("records model, action, and turn failures as errors", async () => { + const provider = braintrustEveInstrumentation(); + const turnContext = providerContext(); + const modelContext = providerContext(); + const actionContext = providerContext(); + const scope = ATTEMPT_SCOPE; - await wildcard?.( + await provider.events["turn.started"]( { - data: { sequence: 0, turnId: "turn-selective-flush" }, + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-root", + turnId: "turn-0", type: "turn.started", }, - ctx, + turnContext, ); - const flushSpy = vi - .spyOn(backgroundLogger, "flush") - .mockResolvedValue(undefined); - - await wildcard?.( + await provider.events["model.call.started"]( { - data: { wait: "next-user-message" }, - type: "session.waiting", + idempotencyKey: "model:attempt-0:0", + model: { modelId: "gpt-5.4-mini", provider: "openai.responses" }, + scope, + type: "model.call.started", }, - ctx, + modelContext, ); - expect(flushSpy).not.toHaveBeenCalled(); - - await wildcard?.( + await provider.events["model.call.failed"]( { - data: { - finishReason: "stop", - message: null, - sequence: 0, - stepIndex: 0, - turnId: "turn-selective-flush", - }, - type: "message.completed", + error: new Error("model exploded"), + idempotencyKey: "model:attempt-0:0", + scope, + type: "model.call.failed", }, - ctx, + modelContext, ); - expect(flushSpy).not.toHaveBeenCalled(); - - await wildcard?.( + await provider.events["action.started"]( { - type: "session.completed", + callId: "call-failing", + idempotencyKey: "action:session-root:turn-0:call-failing", + name: "failing-tool", + scope, + type: "action.started", }, - ctx, + actionContext, + ); + await provider.events["action.failed"]( + { + error: new Error("tool exploded"), + errorCode: "TOOL_FAILED", + idempotencyKey: "action:session-root:turn-0:call-failing", + outcome: "failed", + scope, + type: "action.failed", + }, + actionContext, + ); + await provider.events["turn.failed"]( + { + error: new Error("turn exploded"), + idempotencyKey: "turn:session-root:turn-0", + sessionId: "session-root", + turnId: "turn-0", + type: "turn.failed", + }, + turnContext, ); - expect(flushSpy).toHaveBeenCalledTimes(1); - }); - - it("serializes events per session without blocking other sessions", async () => { - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - expect(wildcard).toBeDefined(); - - const sessionA: EveHookContext = { session: { id: "session-queue-a" } }; - const sessionB: EveHookContext = { session: { id: "session-queue-b" } }; - const emitA = (event: EveHandleMessageStreamEvent) => - wildcard?.(event, sessionA); - const emitB = (event: EveHandleMessageStreamEvent) => - wildcard?.(event, sessionB); - - await emitA({ - data: { sequence: 0, turnId: "turn-a" }, - type: "turn.started", - }); - await emitB({ - data: { sequence: 0, turnId: "turn-b" }, - type: "turn.started", - }); - - let releaseFirstFlush: (() => void) | undefined; - const firstFlush = new Promise((resolve) => { - releaseFirstFlush = resolve; - }); - const flushSpy = vi - .spyOn(backgroundLogger, "flush") - .mockImplementationOnce(() => firstFlush) - .mockResolvedValue(undefined); - - const doneA = emitA({ - type: "session.completed", - }); - for (let i = 0; i < 10 && flushSpy.mock.calls.length < 1; i++) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } - expect(flushSpy).toHaveBeenCalledTimes(1); - - let queuedEventFinished = false; - const queuedA = Promise.resolve( - emitA({ - data: { - finishReason: "stop", - message: "queued", - sequence: 0, - stepIndex: 0, - turnId: "turn-a", - }, - type: "message.completed", - }), - ).then(() => { - queuedEventFinished = true; - }); - await Promise.resolve(); - expect(queuedEventFinished).toBe(false); - - await emitB({ - data: { sequence: 0, turnId: "turn-b" }, - type: "turn.completed", - }); - expect(queuedEventFinished).toBe(false); - releaseFirstFlush?.(); - await Promise.all([doneA, queuedA]); - expect(queuedEventFinished).toBe(true); + const spans = (await backgroundLogger.drain()) as Array< + Record + >; + expect( + spans.find((span) => span.span_attributes?.name === "eve.step")?.error, + ).toContain("model exploded"); + expect( + spans.find((span) => span.span_attributes?.name === "failing-tool") + ?.error, + ).toContain("tool exploded"); + expect( + spans.find((span) => span.span_attributes?.name === "eve.turn")?.error, + ).toContain("turn exploded"); }); - it("uses deterministic ids and attaches local subagent turns to their tool span", async () => { - const parentEveState = createFakeDefineState(); - const childEveState = createFakeDefineState(); - const parentWildcard = braintrustEveHook({ - defineState: parentEveState.defineState, - }).events?.["*"]; - const childWildcard = braintrustEveHook({ - defineState: childEveState.defineState, - }).events?.["*"]; - expect(parentWildcard).toBeDefined(); - expect(childWildcard).toBeDefined(); - - const parentCtx: EveHookContext = { - session: { id: "session-parent" }, - }; - const childCtx: EveHookContext = { - session: { - id: "session-child", - parent: { - callId: "call-researcher", - sessionId: "session-parent", - turn: { id: "turn-parent" }, - }, - }, - }; - const emitParent = (event: EveHandleMessageStreamEvent) => - parentWildcard?.(event, parentCtx); - const emitChild = (event: EveHandleMessageStreamEvent) => - childWildcard?.(event, childCtx); + it("projects model input without provider-private fields", async () => { + const provider = braintrustEveInstrumentation(); + const turnContext = providerContext(); + const modelContext = providerContext(); - await emitParent({ - data: { sequence: 0, turnId: "turn-parent" }, - type: "turn.started", - }); - await emitParent({ - data: { - message: "Research Eve tracing", + await provider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", sequence: 0, - turnId: "turn-parent", + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", }, - type: "message.received", - }); - await emitParent({ - data: { sequence: 0, stepIndex: 0, turnId: "turn-parent" }, - type: "step.started", - }); - await emitParent({ - data: { - actions: [ - { - callId: "call-researcher", - input: { message: "Find the relevant section" }, - kind: "subagent-call", - name: "researcher", - subagentName: "researcher", + turnContext, + ); + await provider.events["model.call.started"]( + { + idempotencyKey: "model:attempt-0:0", + input: { + instructions: { + content: "Object system instruction", + providerOptions: { encrypted: "instruction-secret" }, + role: "system", }, - ], - sequence: 0, - stepIndex: 0, - turnId: "turn-parent", - }, - type: "actions.requested", - }); - await emitParent({ - data: { - callId: "call-researcher", - childSessionId: "session-child", - name: "researcher", - sequence: 0, - toolName: "researcher", - turnId: "turn-parent", + messages: [ + { + content: "Hello", + providerOptions: { reasoning_details: "message-secret" }, + role: "user", + }, + { + content: [ + { + providerOptions: { encrypted: "part-secret" }, + text: "Thinking", + type: "reasoning", + }, + { + input: { query: "Eve" }, + providerOptions: { encrypted: "tool-secret" }, + toolCallId: "call-0", + toolName: "search", + type: "tool-call", + }, + { + providerOptions: { encrypted: "custom-secret" }, + type: "custom", + }, + ], + providerOptions: { reasoning_details: "assistant-secret" }, + role: "assistant", + }, + ], + }, + model: { modelId: "gpt-5.4-mini", provider: "openai.responses" }, + scope: ATTEMPT_SCOPE, + type: "model.call.started", }, - type: "subagent.called", - }); + modelContext, + ); - await emitChild({ - data: { sequence: 0, turnId: "turn-child" }, - type: "turn.started", - }); - await emitChild({ - data: { - message: "Find the relevant section", - sequence: 0, - turnId: "turn-child", - }, - type: "message.received", - }); - await emitChild({ - data: { sequence: 0, stepIndex: 0, turnId: "turn-child" }, - type: "step.started", - }); - await emitChild({ - data: { - actions: [ + const spans = (await backgroundLogger.drain()) as Array< + Record + >; + const step = spans.find( + (span) => span.span_attributes?.name === "eve.step", + ); + expect(step?.input).toEqual([ + { content: "Object system instruction", role: "system" }, + { content: "Hello", role: "user" }, + { + content: [ + { text: "Thinking", type: "reasoning" }, { - callId: "call-search", - input: { query: "nested eve" }, - kind: "tool-call", + input: { query: "Eve" }, + toolCallId: "call-0", toolName: "search", + type: "tool-call", }, ], - sequence: 0, - stepIndex: 1, - turnId: "turn-child", - }, - type: "actions.requested", - }); - await emitChild({ - data: { - result: { - callId: "call-search", - kind: "tool-result", - output: { title: "Nested Eve" }, - toolName: "search", - }, - sequence: 0, - status: "completed", - stepIndex: 1, - turnId: "turn-child", - }, - type: "action.result", - }); - await emitChild({ - data: { - finishReason: "stop", - message: "Child found Nested Eve.", - sequence: 0, - stepIndex: 0, - turnId: "turn-child", - }, - type: "message.completed", - }); - await emitChild({ - data: { - finishReason: "stop", - sequence: 0, - stepIndex: 0, - turnId: "turn-child", + role: "assistant", }, - type: "step.completed", - }); - await emitChild({ - data: { sequence: 0, turnId: "turn-child" }, - type: "turn.completed", - }); + ]); + expect(JSON.stringify(step?.input)).not.toContain("providerOptions"); + expect(JSON.stringify(step?.input)).not.toContain("secret"); + }); - await emitParent({ - data: { - callId: "call-researcher", - output: "Child found Nested Eve.", + it("converts inline model and tool-result media to attachments", async () => { + const provider = braintrustEveInstrumentation(); + const actionContext = providerContext(); + await provider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", sequence: 0, - status: "completed", - subagentName: "researcher", - turnId: "turn-parent", + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", }, - type: "subagent.completed", - }); - await emitParent({ - data: { - result: { - callId: "call-researcher", - kind: "subagent-result", - output: "Child found Nested Eve.", - subagentName: "researcher", + providerContext(), + ); + await provider.events["model.call.started"]( + { + idempotencyKey: "model:attempt-0:0", + input: { + messages: [ + { + content: [ + { + data: "AQID", + filename: "tiny.png", + mediaType: "image/png", + type: "file", + }, + { + image: "DQ4P", + mediaType: "image/png", + type: "image", + }, + { + data: "not valid base64!", + filename: "invalid.png", + mediaType: "image/png", + type: "file", + }, + ], + role: "user", + }, + { + content: [ + { + output: { + type: "content", + value: [ + { + data: "BAUG", + mediaType: "image/png", + type: "image-data", + }, + ], + }, + toolCallId: "call-image", + toolName: "render", + type: "tool-result", + }, + ], + role: "tool", + }, + ], }, - sequence: 0, - status: "completed", - stepIndex: 0, - turnId: "turn-parent", - }, - type: "action.result", - }); - await emitParent({ - data: { sequence: 0, stepIndex: 1, turnId: "turn-parent" }, - type: "step.started", - }); - await emitParent({ - data: { - actions: [ - { - callId: "call-read", - input: { url: "https://eve.dev/docs/guides/instrumentation" }, - kind: "tool-call", - toolName: "read", - }, - ], - sequence: 0, - stepIndex: 1, - turnId: "turn-parent", + model: { modelId: "gpt-5.4-mini", provider: "openai.responses" }, + scope: ATTEMPT_SCOPE, + type: "model.call.started", }, - type: "actions.requested", - }); - await emitParent({ - data: { - result: { - callId: "call-read", - kind: "tool-result", - output: { title: "Runtime context" }, - toolName: "read", + providerContext(), + ); + await provider.events["action.started"]( + { + callId: "call-render", + idempotencyKey: "action:session-root:turn-0:call-render", + input: { + data: "data:image/png;base64,BwgJ", + mediaType: "image/png", + type: "file", }, - sequence: 0, - status: "completed", - stepIndex: 1, - turnId: "turn-parent", - }, - type: "action.result", - }); - await emitParent({ - data: { - finishReason: "tool-calls", - sequence: 0, - stepIndex: 1, - turnId: "turn-parent", - }, - type: "step.completed", - }); - await emitParent({ - data: { sequence: 0, stepIndex: 2, turnId: "turn-parent" }, - type: "step.started", - }); - await emitParent({ - data: { - finishReason: "stop", - message: "Parent used the child result.", - sequence: 0, - stepIndex: 2, - turnId: "turn-parent", + name: "render", + scope: ATTEMPT_SCOPE, + type: "action.started", }, - type: "message.completed", - }); - await emitParent({ - data: { - finishReason: "stop", - sequence: 0, - stepIndex: 2, - turnId: "turn-parent", + actionContext, + ); + await provider.events["action.completed"]( + { + idempotencyKey: "action:session-root:turn-0:call-render", + output: { + output: { + type: "content", + value: [ + { + data: new Uint8Array([10, 11, 12]), + mediaType: "image/png", + type: "image-data", + }, + ], + }, + type: "result", + }, + scope: ATTEMPT_SCOPE, + type: "action.completed", }, - type: "step.completed", - }); - await emitParent({ - data: { sequence: 0, turnId: "turn-parent" }, - type: "turn.completed", - }); + actionContext, + ); const spans = (await backgroundLogger.drain()) as Array< Record >; - const parentTurnId = deterministicEveIdForTest( - "eve:turn", - "session-parent", - "turn-parent", - ); - const childTurnId = deterministicEveIdForTest( - "eve:turn", - "session-child", - "turn-child", - ); - const subagentSpanId = deterministicEveIdForTest( - "eve:subagent", - "session-parent", - "call-researcher", - ); - const parentTurn = spans.find( - (span) => - span.span_attributes?.name === "eve.turn" && - span.span_id === parentTurnId, - ); - const subagentSpans = spans.filter( - (span) => span.span_attributes?.name === "researcher", - ); - const childTurn = spans.find( - (span) => - span.span_attributes?.name === "eve.turn" && - span.span_id === childTurnId, - ); - const childSearch = spans.find( - (span) => - span.span_attributes?.name === "search" && - span.span_parents?.[0] === childTurnId, - ); - const parentRead = spans.find( - (span) => - span.span_attributes?.name === "read" && - span.span_parents?.[0] === parentTurnId, - ); - const parentSteps = spans.filter( - (span) => - span.span_attributes?.name === "eve.step" && - span.span_parents?.[0] === parentTurnId, - ); - - expect(parentTurn).toBeDefined(); - expect(subagentSpans).toHaveLength(1); - expect(subagentSpans[0]?.span_id).toBe(subagentSpanId); - expect(subagentSpans[0]?.input).toEqual({ - message: "Find the relevant section", - }); - expect(childTurn).toBeDefined(); - expect(childSearch).toBeDefined(); - expect(parentRead).toBeDefined(); - expect(parentSteps).toHaveLength(3); - expect(parentTurn?.span_parents).toEqual([]); - expect(parentTurn?.span_id).toBe(parentTurnId); - expect(parentTurn?.span_id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ); - expect(parentTurn?.root_span_id).toMatch( - /^([0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/, - ); - expect(parentTurn?.root_span_id).toBe( - deterministicEveIdForTest("eve:root", "session-parent", "turn-parent"), - ); - expect(subagentSpans[0]?.span_parents).toEqual([parentTurn?.span_id]); - expect(childTurn?.span_parents).toEqual([subagentSpanId]); - expect(childTurn?.root_span_id).toBe(parentTurn?.root_span_id); - expect(parentTurn?.metadata).toEqual({ - "eve.session_id": "session-parent", - }); - expect(subagentSpans[0]?.metadata).toEqual({ - "eve.session_id": "session-parent", - }); - expect(childTurn?.metadata).toEqual({ - "eve.session_id": "session-child", - }); - expect(childSearch?.metadata).toEqual({ - "eve.session_id": "session-child", - }); - expect(parentRead?.metadata).toEqual({ - "eve.session_id": "session-parent", - }); - expect(childSearch?.span_parents).toEqual([childTurn?.span_id]); - expect(childSearch?.span_id).toBe( - deterministicEveIdForTest( - "eve:tool", - "session-child", - "turn-child", - "call-search", - ), + const step = spans.find( + (span) => span.span_attributes?.name === "eve.step", ); - expect(parentRead?.span_id).toBe( - deterministicEveIdForTest( - "eve:tool", - "session-parent", - "turn-parent", - "call-read", - ), + const inputAttachment = step?.input?.[0]?.content?.[0]?.data; + const imageAttachment = step?.input?.[0]?.content?.[1]?.image; + const unconvertedFile = step?.input?.[0]?.content?.[2]?.data; + const outputAttachment = + step?.input?.[1]?.content?.[0]?.output?.value?.[0]?.data; + const action = spans.find( + (span) => span.span_attributes?.name === "render", ); - expect(parentSteps.map((span) => span.span_id)).toEqual([ - deterministicEveIdForTest( - "eve:step", - "session-parent", - "turn-parent", - "0", - ), - deterministicEveIdForTest( - "eve:step", - "session-parent", - "turn-parent", - "1", - ), - deterministicEveIdForTest( - "eve:step", - "session-parent", - "turn-parent", - "2", - ), - ]); - expect(spans.map((span) => span.span_attributes?.name)).toEqual([ - "eve.turn", - "eve.step", - "researcher", - "eve.turn", - "eve.step", - "search", - "eve.step", - "read", - "eve.step", - ]); + const actionInputAttachment = action?.input?.data; + const actionOutputAttachment = action?.output?.value?.[0]?.data; + expect(inputAttachment).toBeInstanceOf(Attachment); + expect(inputAttachment.reference).toMatchObject({ + content_type: "image/png", + filename: "tiny.png", + type: "braintrust_attachment", + }); + expect(imageAttachment).toBeInstanceOf(Attachment); + expect(imageAttachment.reference).toMatchObject({ + content_type: "image/png", + type: "braintrust_attachment", + }); + expect(unconvertedFile).toBe("not valid base64!"); + expect(outputAttachment).toBeInstanceOf(Attachment); + expect(outputAttachment.reference).toMatchObject({ + content_type: "image/png", + filename: "attachment.png", + type: "braintrust_attachment", + }); + expect(actionInputAttachment).toBeInstanceOf(Attachment); + expect(actionInputAttachment.reference).toMatchObject({ + content_type: "image/png", + type: "braintrust_attachment", + }); + expect(actionOutputAttachment).toBeInstanceOf(Attachment); + expect(actionOutputAttachment.reference).toMatchObject({ + content_type: "image/png", + type: "braintrust_attachment", + }); + expect(JSON.stringify(step?.input)).not.toContain("AQID"); + expect(JSON.stringify(step?.input)).not.toContain("BAUG"); + }); - backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - initLogger({ - projectName: "eve-plugin.test.ts", - projectId: "test-project-id", - }); - const replay = braintrustEveHook({ - defineState: parentEveState.defineState, - }).events?.["*"]; - await replay?.( + it("preserves arbitrary action outputs with type fields", async () => { + const provider = braintrustEveInstrumentation(); + const actionContext = providerContext(); + await provider.events["turn.started"]( { - data: { sequence: 0, turnId: "turn-parent" }, + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-root", + turnId: "turn-0", type: "turn.started", }, - parentCtx, + providerContext(), ); - await replay?.( + await provider.events["action.started"]( { - data: { sequence: 0, turnId: "turn-parent" }, - type: "turn.completed", + callId: "call-text", + idempotencyKey: "action:session-root:turn-0:call-text", + name: "text-result", + scope: ATTEMPT_SCOPE, + type: "action.started", + }, + actionContext, + ); + await provider.events["action.completed"]( + { + idempotencyKey: "action:session-root:turn-0:call-text", + output: { + output: { text: "preserve me", type: "text" }, + type: "result", + }, + scope: ATTEMPT_SCOPE, + type: "action.completed", }, - parentCtx, + actionContext, ); - const replaySpans = (await backgroundLogger.drain()) as Array< + + const spans = (await backgroundLogger.drain()) as Array< Record >; expect( - replaySpans.find((span) => span.span_id === parentTurn?.span_id), - ).toMatchObject({ _is_merge: true }); + spans.find((span) => span.span_attributes?.name === "text-result") + ?.output, + ).toEqual({ text: "preserve me", type: "text" }); }); - it("does not parent an Eve turn under the active Braintrust span", async () => { - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - expect(wildcard).toBeDefined(); + it.each([ + ["step.attempt.completed", undefined], + ["step.attempt.failed", new Error("attempt exploded")], + ] as const)("closes an open model span on %s", async (type, error) => { + const provider = braintrustEveInstrumentation(); + await provider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", + }, + providerContext(), + ); + await provider.events["model.call.started"]( + { + idempotencyKey: "model:attempt-0:0", + model: { modelId: "gpt-5.4-mini", provider: "openai.responses" }, + scope: ATTEMPT_SCOPE, + type: "model.call.started", + }, + providerContext(), + ); - const ctx: EveHookContext = { - session: { id: "session-wrapped" }, - }; - const parent = startSpan({ name: "workflow" }); - await withCurrent(parent, async () => { - await wildcard?.( + if (type === "step.attempt.completed") { + await provider.events[type]( { - data: { sequence: 0, turnId: "turn-wrapped" }, - type: "turn.started", + idempotencyKey: "step:attempt-0", + scope: ATTEMPT_SCOPE, + type, }, - ctx, + providerContext(), ); - await wildcard?.( + } else { + await provider.events[type]( { - data: { sequence: 0, turnId: "turn-wrapped" }, - type: "turn.completed", + error, + idempotencyKey: "step:attempt-0", + scope: ATTEMPT_SCOPE, + type, }, - ctx, + providerContext(), ); - }); - parent.end(); + } const spans = (await backgroundLogger.drain()) as Array< Record >; - const turn = spans.find( - (span) => span.span_attributes?.name === "eve.turn", - ); - expect(turn?.span_id).toBe( - deterministicEveIdForTest("eve:turn", "session-wrapped", "turn-wrapped"), - ); - expect(turn?.span_parents).toEqual([]); - expect(turn?.root_span_id).toBe( - deterministicEveIdForTest("eve:root", "session-wrapped", "turn-wrapped"), + const step = spans.find( + (span) => span.span_attributes?.name === "eve.step", ); - expect(turn?.root_span_id).not.toBe(parent.rootSpanId); - }); - - it("does not throw when Eve emits malformed events or failures", async () => { - const wildcard = braintrustEveHook({ defineState }).events?.["*"]; - expect(wildcard).toBeDefined(); - - await expect( - wildcard?.({ bad: true } as never, {} as never), - ).resolves.toBeUndefined(); - await expect( - wildcard?.( - { - data: { - code: "boom", - message: "step failed", - sequence: 0, - stepIndex: 0, - turnId: "turn-missing-session", - }, - type: "step.failed", - }, - {} as never, - ), - ).resolves.toBeUndefined(); - await expect( - wildcard?.( - { - data: { runtime: { modelId: 123 } }, - type: "session.started", - } as never, - { session: { id: "session-malformed-runtime" } }, - ), - ).resolves.toBeUndefined(); - - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - expect(spans).toEqual([]); + expect(step?.metrics?.end).toEqual(expect.any(Number)); + if (error !== undefined) { + expect(step?.error).toContain("attempt exploded"); + } }); }); diff --git a/js/src/instrumentation/plugins/eve-plugin.ts b/js/src/instrumentation/plugins/eve-plugin.ts index ef18f8913..8729c6a48 100644 --- a/js/src/instrumentation/plugins/eve-plugin.ts +++ b/js/src/instrumentation/plugins/eve-plugin.ts @@ -1,7 +1,9 @@ import { debugLogger } from "../../debug-logger"; import { + Attachment, NOOP_SPAN, _internalStartSpanWithInitialMerge, + currentLogger, flush, updateSpan, withCurrent, @@ -14,161 +16,105 @@ import { } from "../../span-origin"; import { SpanTypeAttribute, isObject } from "../../../util/index"; import { getCurrentUnixTimestamp } from "../../util"; +import { + convertDataToBlob, + getExtensionFromMediaType, + processInputAttachments, +} from "../../wrappers/attachment-utils"; import type { - EveAssistantStepFinishReason, - EveActionResultError, - EveHandleMessageStreamEvent, - EveHookContext, - EveHookDefinition, + EveInstrumentationActionCompletedEvent, + EveInstrumentationActionFailedEvent, + EveInstrumentationActionStartedEvent, + EveInstrumentationContentPart, EveInstrumentationDefinition, + EveInstrumentationHandlerContext, + EveInstrumentationModelCallCompletedEvent, + EveInstrumentationModelCallFailedEvent, + EveInstrumentationModelCallStartedEvent, EveInstrumentationModelInput, - EveInstrumentationStepStartedEventInput, - EveModelMessage, - EveModelMessageContentPart, - EveRuntimeActionRequest, - EveRuntimeActionResult, - EveRuntimeToolCallActionRequest, - EveRuntimeToolResultActionResult, + EveInstrumentationStepAttemptCompletedEvent, + EveInstrumentationStepAttemptFailedEvent, + EveInstrumentationTurnFailedEvent, + EveInstrumentationTurnSettledEvent, + EveInstrumentationTurnStartedEvent, + EveInstrumentationUsage, } from "../../vendor-sdk-types/eve"; -type SpanState = { - metadata: Record; - span: EveSpan; -}; - type EveSpan = Pick; -type EveSpanStartEvent = { - readonly created: string; - readonly metrics: { readonly start: number }; - readonly span_attributes: Record; - readonly span_parents: string[]; -}; - type EveSpanReference = { - readonly endTime?: number; readonly exported: string; readonly rootSpanId: string; - readonly rowId: string; readonly spanId: string; - readonly startEvent?: EveSpanStartEvent; -}; - -type StepState = SpanState & { - input?: unknown; - metrics: Record; - output?: unknown; - reasoning: readonly EveReasoningBlock[]; + readonly turnMetricContributions?: Record>; }; -type TurnState = SpanState & { +type TurnState = { key: string; - metrics: Record; - output?: unknown; - sessionId: string; - stepsByIndex: Map; - turnId: string; -}; - -type ToolState = SpanState & { - endedByTurn?: boolean; - turnKey: string; -}; - -type EveEntityKind = "step" | "subagent" | "tool" | "turn"; - -type EveStateHandle = { - get(): T; - update(fn: (current: T) => T): void; -}; - -type EveDefineState = (name: string, initial: () => T) => EveStateHandle; - -type EveTraceState = { metadata: Record; - reasoningBlocks: readonly (EveReasoningBlock & { key: string })[]; - spanReferences: readonly EveSpanReference[]; - stepStarts: readonly { - ordinal: number; - open: boolean; - stepIndex: number; - turnId: string; - }[]; - llmInputs: readonly { - input: CapturedEveModelInput; - key: string; - }[]; + rootSpanId: string; + rowId: string; + span: EveSpan; + spanId: string; }; -type EveReasoningBlock = { - content: string; - eventAt?: string; -}; +type EveInstrumentationEvent = + | EveInstrumentationActionCompletedEvent + | EveInstrumentationActionFailedEvent + | EveInstrumentationActionStartedEvent + | EveInstrumentationModelCallCompletedEvent + | EveInstrumentationModelCallFailedEvent + | EveInstrumentationModelCallStartedEvent + | EveInstrumentationStepAttemptCompletedEvent + | EveInstrumentationStepAttemptFailedEvent + | EveInstrumentationTurnFailedEvent + | EveInstrumentationTurnSettledEvent + | EveInstrumentationTurnStartedEvent; -const EVE_TRACE_STATE_KEY = "braintrust.eve.tracing"; const MAX_EVE_CACHE_ENTRIES = 10_000; -const MAX_STORED_LLM_INPUTS = 100; -const MAX_STORED_REASONING_BLOCKS = 100; -const MAX_STORED_SPAN_REFERENCES = 10_000; -const MAX_STORED_STEP_STARTS = 10_000; - -type CapturedEveModelMessage = { - content: string | readonly Record[]; - role: EveModelMessage["role"]; -}; - -type CapturedEveModelInput = readonly CapturedEveModelMessage[]; - -/** Manual hook instrumentation for eve runtime stream events. */ -export function braintrustEveHook(options: { - defineState: EveDefineState; - metadata?: Record; -}): EveHookDefinition { - const state = options.defineState(EVE_TRACE_STATE_KEY, emptyEveTraceState); - const bridge = new EveBridge(state); - return { - events: { - "*": async (event: EveHandleMessageStreamEvent, ctx: EveHookContext) => { - await bridge.handle(event, ctx, options.metadata); - }, - }, - }; -} - -/** Eve instrumentation helper for logger setup and durable LLM input capture. */ -export function braintrustEveInstrumentation(options: { - defineState: EveDefineState; - setup?: EveInstrumentationDefinition["setup"]; -}): EveInstrumentationDefinition { - const state = options.defineState(EVE_TRACE_STATE_KEY, emptyEveTraceState); +const eveRootSpanIdsByTurnKey = new LRUCache({ + max: MAX_EVE_CACHE_ENTRIES, +}); + +/** + * Creates an Eve instrumentation provider. + * + * Eve 0.34.0 introduced the provider event API used here. Older Eve hook and + * single-file instrumentation layouts are intentionally unsupported. + */ +export function braintrustEveInstrumentation( + options: { + metadata?: Record; + setup?: EveInstrumentationDefinition["setup"]; + } = {}, +): EveInstrumentationDefinition { + const bridge = new EveBridge(options.metadata); return { + capture: "content", events: { - "step.started": (input: EveInstrumentationStepStartedEventInput) => { - try { - captureEveModelInput(state, input); - } catch (error) { - debugLogger.warn("Error in Eve LLM input capture:", error); - } - }, + "action.completed": (event, context) => bridge.handle(event, context), + "action.failed": (event, context) => bridge.handle(event, context), + "action.started": (event, context) => bridge.handle(event, context), + "model.call.completed": (event, context) => bridge.handle(event, context), + "model.call.failed": (event, context) => bridge.handle(event, context), + "model.call.started": (event, context) => bridge.handle(event, context), + "step.attempt.completed": (event, context) => + bridge.handle(event, context), + "step.attempt.failed": (event, context) => bridge.handle(event, context), + "turn.cancelled": (event, context) => bridge.handle(event, context), + "turn.completed": (event, context) => bridge.handle(event, context), + "turn.failed": (event, context) => bridge.handle(event, context), + "turn.started": (event, context) => bridge.handle(event, context), }, - recordInputs: false, - recordOutputs: false, + flush: () => flush(), setup: options.setup, }; } -function isEveHandleMessageStreamEvent( - event: unknown, -): event is EveHandleMessageStreamEvent { - return isObject(event) && typeof event["type"] === "string"; -} - class ResumedEveSpan implements EveSpan { private endTime: number | undefined; - constructor(private readonly reference: EveSpanReference) { - this.endTime = reference.endTime; - } + constructor(private readonly reference: EveSpanReference) {} get rootSpanId(): string { return this.reference.rootSpanId; @@ -179,16 +125,9 @@ class ResumedEveSpan implements EveSpan { } log(event: Parameters[0]): void { - const metrics = { - ...this.reference.startEvent?.metrics, - ...(this.endTime === undefined ? {} : { end: this.endTime }), - ...event.metrics, - }; updateSpan({ exported: this.reference.exported, - ...this.reference.startEvent, ...event, - ...(Object.keys(metrics).length > 0 ? { metrics } : {}), }); } @@ -202,2109 +141,1061 @@ class ResumedEveSpan implements EveSpan { } class EveBridge { - constructor(private readonly state: EveStateHandle) {} - - private eventQueuesBySession = new Map>(); - private completedToolKeys = new LRUCache({ - max: MAX_EVE_CACHE_ENTRIES, - }); - private toolsByCallKey = new LRUCache({ - max: MAX_EVE_CACHE_ENTRIES, - }); private turnsByKey = new LRUCache({ max: MAX_EVE_CACHE_ENTRIES, }); + private turnsStartingByKey = new Map>(); + private openModelsByAttempt = new Map>(); + private turnMetricContributionsByKey = new Map< + string, + Record> + >(); - private async startEveSpan( - args: Parameters[0], - ): Promise { - const rowId = args?.event?.id; - const reference = - typeof rowId === "string" && - readEveTraceState(this.state).spanReferences.find( - (candidate) => candidate.rowId === rowId, - ); - if (reference) { - return new ResumedEveSpan(reference); - } - - const startTime = args?.startTime ?? getCurrentUnixTimestamp(); - const parentSpanIds = args?.parentSpanIds; - const startEvent: EveSpanStartEvent = { - created: new Date().toISOString(), - metrics: { start: startTime }, - span_attributes: { - ...(args?.name ? { name: args.name } : {}), - ...(args?.type ? { type: args.type } : {}), - ...args?.spanAttributes, - }, - span_parents: parentSpanIds - ? "spanId" in parentSpanIds - ? [parentSpanIds.spanId] - : parentSpanIds.parentSpanIds - : [], - }; - const span = withCurrent(NOOP_SPAN, () => - _internalStartSpanWithInitialMerge( - withSpanInstrumentationName( - { ...args, startTime }, - INSTRUMENTATION_NAMES.EVE, - ), - ), - ); - if (typeof rowId !== "string") { - return span; - } + constructor(private readonly configuredMetadata?: Record) {} + async handle( + event: EveInstrumentationEvent, + context: EveInstrumentationHandlerContext, + ): Promise { try { - const exported = await span.export(); - const reference = { - exported, - rootSpanId: span.rootSpanId, - rowId, - spanId: span.spanId, - startEvent, - }; - this.state.update((current) => { - const normalized = normalizeEveTraceState(current); - return normalized.spanReferences.some( - (candidate) => candidate.rowId === rowId, - ) - ? normalized - : { - ...normalized, - spanReferences: [...normalized.spanReferences, reference].slice( - -MAX_STORED_SPAN_REFERENCES, - ), - }; - }); + switch (event.type) { + case "turn.started": + await this.getOrStartTurn(event, context); + return; + case "model.call.started": + await this.handleModelCallStarted(event, context); + return; + case "model.call.completed": + await this.handleModelCallCompleted(event, context); + return; + case "model.call.failed": + await this.handleModelCallFailed(event, context); + return; + case "step.attempt.completed": + case "step.attempt.failed": + this.handleStepAttemptTerminal(event); + return; + case "action.started": + await this.handleActionStarted(event, context); + return; + case "action.completed": + await this.handleActionCompleted(event, context); + return; + case "action.failed": + await this.handleActionFailed(event, context); + return; + case "turn.cancelled": + case "turn.completed": + await this.handleTurnSettled(event, context); + return; + case "turn.failed": + await this.handleTurnFailed(event, context); + return; + } } catch (error) { - debugLogger.warn("Error exporting Eve span for resumption:", error); + debugLogger.warn("Error in Eve instrumentation provider:", error); } - return span; } - private async startEveChildSpan( - parent: EveSpan, - args: Parameters[0], - ): Promise { - return await this.startEveSpan({ - ...args, + private async handleModelCallStarted( + event: EveInstrumentationModelCallStartedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const turn = await this.turnForScope(event.scope, context); + const metadata = { + ...this.metadata(event.scope.sessionId), + ...modelMetadata(event.model.modelId, event.model.provider), + }; + const input = capturedModelInput(event.input); + const ids = await generateEveIds("step", event.idempotencyKey); + const span = await this.startEveSpan(context, { + event: { + id: ids.rowId, + ...(input === undefined ? {} : { input }), + metadata, + }, + name: "eve.step", parentSpanIds: { - rootSpanId: parent.rootSpanId, - spanId: parent.spanId, + rootSpanId: turn.rootSpanId, + spanId: turn.spanId, }, + spanAttributes: { type: SpanTypeAttribute.LLM }, + spanId: ids.spanId, }); - } + const reference = readSpanReference(context.state.get()); + if (reference !== undefined) { + context.state.set({ + ...reference, + turnMetricContributions: + this.turnMetricContributionsByKey.get(turn.key) ?? {}, + }); + } + const openModels = + this.openModelsByAttempt.get(event.scope.attemptId) ?? new Map(); + openModels.set(event.idempotencyKey, span); + this.openModelsByAttempt.set(event.scope.attemptId, openModels); - private stepOrdinal( - event: Extract, - ): number { - let ordinal = 0; - this.state.update((current) => { - const state = normalizeEveTraceState(current); - const previous = state.stepStarts - .filter( - (entry) => - entry.turnId === event.data.turnId && - entry.stepIndex === event.data.stepIndex, - ) - .at(-1); - if (previous?.open) { - ordinal = previous.ordinal; - return state; + if (input !== undefined) { + const initialInput = initialTurnInput(input); + if (initialInput !== undefined) { + this.updateTurn(turn, { input: initialInput }); } - - ordinal = state.stepStarts.filter( - (entry) => entry.turnId === event.data.turnId, - ).length; - return { - ...state, - stepStarts: [ - ...state.stepStarts, - { - open: true, - ordinal, - stepIndex: event.data.stepIndex, - turnId: event.data.turnId, - }, - ].slice(-MAX_STORED_STEP_STARTS), - }; - }); - return ordinal; + } } - private markStepEnded(turnId: string, stepIndex: number): void { - this.state.update((current) => { - const state = normalizeEveTraceState(current); - let index = -1; - for (let i = state.stepStarts.length - 1; i >= 0; i--) { - const entry = state.stepStarts[i]; - if (entry?.turnId === turnId && entry.stepIndex === stepIndex) { - index = i; - break; - } - } - if (index < 0 || !state.stepStarts[index]?.open) { - return state; - } - return { - ...state, - stepStarts: state.stepStarts.map((entry, entryIndex) => - entryIndex === index ? { ...entry, open: false } : entry, - ), - }; - }); + private async handleModelCallCompleted( + event: EveInstrumentationModelCallCompletedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const turn = await this.turnForScope(event.scope, context); + this.restoreTurnMetricContributions(turn.key, context.state.get()); + const span = await this.resumeModelSpan(event, context, turn); + const metrics = usageMetrics(event.usage); + const output = modelOutput(event.finishReason, event.content); + span.log({ metrics, output }); + span.end(); + this.closeOpenModel(event.scope.attemptId, event.idempotencyKey); + this.updateTurnMetrics(turn, event.idempotencyKey, metrics); + + const finalOutput = finalText(event.content); + if ( + finalOutput !== undefined && + normalizedFinishReason(event.finishReason) !== "tool_calls" + ) { + this.updateTurn(turn, { output: finalOutput }); + } } - async handle( - event: unknown, - ctx: unknown, - hookMetadata?: Record, + private async handleModelCallFailed( + event: EveInstrumentationModelCallFailedEvent, + context: EveInstrumentationHandlerContext, ): Promise { - if (!isEveHandleMessageStreamEvent(event)) { - return; - } - const run = async () => { - try { - if (!(await this.handleEvent(event, ctx, hookMetadata))) { - return; - } - if (event.type === "session.failed") { - const sessionId = event.data.sessionId || sessionIdFromContext(ctx); - await this.flushInstrumentation(); - if (sessionId) { - this.cleanupSession(sessionId); - } - } else if (event.type === "session.completed") { - const sessionId = sessionIdFromContext(ctx); - await this.flushInstrumentation(); - if (sessionId) { - this.cleanupSession(sessionId); - } - } - } catch (error) { - debugLogger.warn("Error in Eve hook instrumentation:", error); - } - }; + const turn = await this.turnForScope(event.scope, context); + const span = await this.resumeModelSpan(event, context, turn); + span.log({ error: toError(event.error, "Eve model call failed") }); + span.end(); + this.closeOpenModel(event.scope.attemptId, event.idempotencyKey); + } - const sessionId = - event.type === "session.failed" - ? event.data.sessionId || sessionIdFromContext(ctx) - : sessionIdFromContext(ctx); - if (!sessionId) { - await run(); + private handleStepAttemptTerminal( + event: + | EveInstrumentationStepAttemptCompletedEvent + | EveInstrumentationStepAttemptFailedEvent, + ): void { + const openModels = this.openModelsByAttempt.get(event.scope.attemptId); + if (openModels === undefined) { return; } - - const previous = this.eventQueuesBySession.get(sessionId); - const queued = previous ? previous.then(run) : run(); - this.eventQueuesBySession.set(sessionId, queued); - try { - await queued; - } finally { - if (this.eventQueuesBySession.get(sessionId) === queued) { - this.eventQueuesBySession.delete(sessionId); + for (const span of openModels.values()) { + if (event.type === "step.attempt.failed") { + span.log({ + error: toError(event.error, "Eve step attempt failed"), + }); } + span.end(); } + this.openModelsByAttempt.delete(event.scope.attemptId); } - private async handleEvent( - event: EveHandleMessageStreamEvent, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - switch (event.type) { - case "session.started": - this.handleSessionStarted(event, ctx, hookMetadata); - return true; - case "turn.started": - await this.handleTurnStarted(event, ctx, hookMetadata); - return true; - case "message.received": - await this.handleMessageReceived(event, ctx, hookMetadata); - return true; - case "step.started": - await this.handleStepStarted(event, ctx, hookMetadata); - return true; - case "reasoning.completed": - this.handleReasoningCompleted(event, ctx); - return true; - case "message.completed": - this.handleMessageCompleted(event, ctx); - return true; - case "result.completed": - this.handleResultCompleted(event, ctx); - return true; - case "actions.requested": - await this.handleActionsRequested(event, ctx, hookMetadata); - return true; - case "action.result": - await this.handleActionResult(event, ctx, hookMetadata); - return true; - case "subagent.called": - await this.handleSubagentCalled(event, ctx, hookMetadata); - return true; - case "subagent.completed": - await this.handleSubagentCompleted(event, ctx, hookMetadata); - return true; - case "step.completed": - this.handleStepCompleted(event, ctx); - return true; - case "step.failed": - this.handleStepFailed(event, ctx); - return true; - case "turn.completed": - this.handleTurnCompleted(event, ctx); - return true; - case "turn.failed": - this.handleTurnFailed(event, ctx); - return true; - case "session.failed": - this.handleSessionFailed(event, ctx); - return true; - case "session.completed": - this.handleSessionCompleted(event, ctx); - return true; - default: - return false; - } + private async handleActionStarted( + event: EveInstrumentationActionStartedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const turn = await this.turnForScope(event.scope, context); + const metadata = turn.metadata; + const input = processInputAttachments(event.input); + const ids = await generateEveIds( + "action", + event.scope.sessionId, + event.scope.turnId, + event.callId, + ); + await this.startEveSpan(context, { + event: { + id: ids.rowId, + ...(input === undefined ? {} : { input }), + metadata, + }, + name: event.name, + parentSpanIds: { + rootSpanId: turn.rootSpanId, + spanId: turn.spanId, + }, + spanAttributes: { type: SpanTypeAttribute.TOOL }, + spanId: ids.spanId, + }); } - private handleSessionStarted( - event: Extract, - ctx: unknown, - hookMetadata?: Record, - ): void { - const sessionId = sessionIdFromContext(ctx); - if (!sessionId) { - return; + private async handleActionCompleted( + event: EveInstrumentationActionCompletedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const span = await this.resumeActionSpan(event, context); + if (event.output.type === "error") { + span.log({ + error: toError(event.output.error, "Eve action failed"), + }); + } else if (event.output.output !== undefined) { + span.log({ output: processInputAttachments(event.output.output) }); } + span.end(actionEndArgs(event.acceptedAtMs)); + } - const metadata = { - ...(hookMetadata ?? {}), - ...modelMetadataFromRuntime(event.data.runtime), - }; - this.state.update((current) => { - const normalized = normalizeEveTraceState(current); - return { - ...normalized, - metadata: { ...normalized.metadata, ...metadata }, - }; + private async handleActionFailed( + event: EveInstrumentationActionFailedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const span = await this.resumeActionSpan(event, context); + span.log({ + error: toError( + event.error, + event.errorCode === undefined + ? "Eve action " + event.outcome + : event.errorCode + ": Eve action " + event.outcome, + ), }); - for (const [key, turn] of this.turnsByKey) { - if (!key.startsWith(`${sessionId}:`)) { - continue; - } - - turn.metadata = { ...turn.metadata, ...metadata }; - turn.span.log({ metadata: turn.metadata }); - for (const step of turn.stepsByIndex.values()) { - step.metadata = { ...step.metadata, ...metadata }; - step.span.log({ metadata: step.metadata }); - } - } + span.end(actionEndArgs(event.acceptedAtMs)); } - private async handleTurnStarted( - event: Extract, - ctx: unknown, - hookMetadata?: Record, + private async handleTurnSettled( + event: EveInstrumentationTurnSettledEvent, + context: EveInstrumentationHandlerContext, ): Promise { - const sessionId = sessionIdFromContext(ctx); - if (!sessionId) { - return; - } + const turn = await this.resumeTurn(event, context); + turn.span.end(); + this.turnsByKey.delete(turn.key); + this.turnMetricContributionsByKey.delete(turn.key); + } - const key = turnKey(sessionId, event.data.turnId); - const metadata = { - ...readEveTraceState(this.state).metadata, - ...(hookMetadata ?? {}), - "eve.session_id": sessionId, - }; - const existing = this.turnsByKey.get(key); - if (existing) { - existing.metadata = { ...existing.metadata, ...metadata }; - existing.span.log({ metadata: existing.metadata }); - return; - } + private async handleTurnFailed( + event: EveInstrumentationTurnFailedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const turn = await this.resumeTurn(event, context); + turn.span.log({ + error: toError(event.error, "Eve turn failed"), + }); + turn.span.end(); + this.turnsByKey.delete(turn.key); + this.turnMetricContributionsByKey.delete(turn.key); + } - const span = await this.startTurnSpan(sessionId, event, ctx, metadata); - span.log({ metadata }); - this.turnsByKey.set(key, { - key, - metadata, - metrics: {}, - sessionId, - span, - stepsByIndex: new Map(), - turnId: event.data.turnId, + private async startTurnSpan( + event: EveInstrumentationTurnStartedEvent, + context: EveInstrumentationHandlerContext, + metadata: Record, + ): Promise { + const ids = await generateEveIds("turn", event.sessionId, event.turnId); + const parentSpanId = + event.parentLineage === undefined + ? undefined + : await deterministicEveId( + "eve:action", + event.parentLineage.sessionId, + event.parentLineage.turnId, + event.parentLineage.callId, + ); + const parentTurn = + event.parentLineage === undefined + ? undefined + : this.turnsByKey.get( + turnKey(event.parentLineage.sessionId, event.parentLineage.turnId), + ); + const parentRootSpanId = + event.parentLineage === undefined + ? undefined + : eveRootSpanIdsByTurnKey.get( + turnKey(event.parentLineage.sessionId, event.parentLineage.turnId), + ); + const rootSpanId = + event.parentLineage === undefined + ? await deterministicEveId("eve:root", event.sessionId, event.turnId) + : (parentTurn?.rootSpanId ?? + parentRootSpanId ?? + (typeof event.parentTraceContext?.traceId === "string" && + event.parentTraceContext.traceId.length > 0 + ? event.parentTraceContext.traceId + : await deterministicEveId( + "eve:root", + event.parentLineage.sessionId, + event.parentLineage.turnId, + ))); + return await this.startEveSpan(context, { + event: { id: ids.rowId, metadata }, + name: "eve.turn", + parentSpanIds: + parentSpanId === undefined + ? { parentSpanIds: [], rootSpanId } + : { rootSpanId, spanId: parentSpanId }, + spanAttributes: { type: SpanTypeAttribute.TASK }, + spanId: ids.spanId, }); } - private async handleMessageReceived( - event: Extract, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - const turn = await this.ensureTurn(event, ctx, hookMetadata); - if (!turn) { - return; + private async startEveSpan( + context: EveInstrumentationHandlerContext, + args: Parameters[0], + ): Promise { + const reference = readSpanReference(context.state.get()); + if (reference !== undefined) { + return new ResumedEveSpan(reference); } - const input = [{ content: event.data.message, role: "user" }]; - turn.span.log({ input }); + const startTime = args?.startTime ?? getCurrentUnixTimestamp(); + const span = withCurrent(NOOP_SPAN, () => + _internalStartSpanWithInitialMerge( + withSpanInstrumentationName( + { ...args, startTime }, + INSTRUMENTATION_NAMES.EVE, + ), + ), + ); + + try { + const reference: EveSpanReference = { + exported: await span.export(), + rootSpanId: span.rootSpanId, + spanId: span.spanId, + }; + context.state.set(reference); + } catch (error) { + debugLogger.warn("Error exporting Eve span for resumption:", error); + } + return span; } - private async handleStepStarted( - event: Extract, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - const turn = await this.ensureTurn(event, ctx, hookMetadata); - const sessionId = sessionIdFromContext(ctx); - if (!turn || !sessionId) { - return; + private async getOrStartTurn( + event: EveInstrumentationTurnStartedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const key = turnKey(event.sessionId, event.turnId); + const existing = this.turnsByKey.get(key); + if (existing !== undefined) { + return existing; } - const existing = turn.stepsByIndex.get(event.data.stepIndex); - if (existing) { - existing.span.log({ - ...(existing.input !== undefined ? { input: existing.input } : {}), - metadata: existing.metadata, - metrics: existing.metrics, - output: existing.output, - }); - const endTime = eventTime(event); - existing.span.end(endTime === undefined ? undefined : { endTime }); - this.markStepEnded(event.data.turnId, event.data.stepIndex); - clearStoredEveReasoning( - this.state, - sessionId, - event.data.turnId, - event.data.stepIndex, - ); + const starting = this.turnsStartingByKey.get(key); + if (starting !== undefined) { + return await starting; } - const stepOrdinal = this.stepOrdinal(event); - const metadata = { ...turn.metadata }; - const input = consumeCapturedEveModelInput( - this.state, - sessionId, - event.data.turnId, - event.data.stepIndex, - ); - const reasoning = readStoredEveReasoning( - this.state, - sessionId, - event.data.turnId, - event.data.stepIndex, - ); - const output = mergeEveReasoning(undefined, reasoning); - const { rowId: eventId, spanId } = await generateEveIds( - "step", - sessionId, - event.data.turnId, - String(stepOrdinal), - ); - const span = await this.startEveChildSpan(turn.span, { - event: { - id: eventId, - ...(input !== undefined ? { input } : {}), + const metadata = this.metadata(event.sessionId); + const start = (async () => { + const ids = await generateEveIds("turn", event.sessionId, event.turnId); + const span = await this.startTurnSpan(event, context, metadata); + const turn = { + key, metadata, + rootSpanId: span.rootSpanId, + rowId: ids.rowId, + span, + spanId: span.spanId, + }; + this.turnsByKey.set(key, turn); + eveRootSpanIdsByTurnKey.set(key, turn.rootSpanId); + return turn; + })(); + this.turnsStartingByKey.set(key, start); + try { + return await start; + } finally { + if (this.turnsStartingByKey.get(key) === start) { + this.turnsStartingByKey.delete(key); + } + } + } + + private async resumeTurn( + event: + | EveInstrumentationTurnSettledEvent + | EveInstrumentationTurnFailedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const key = turnKey(event.sessionId, event.turnId); + const existing = this.turnsByKey.get(key); + if (existing !== undefined) { + return existing; + } + const starting = this.turnsStartingByKey.get(key); + if (starting !== undefined) { + return await starting; + } + + const metadata = this.metadata(event.sessionId); + const span = await this.startTurnSpan( + { + idempotencyKey: event.idempotencyKey, + rootSessionId: event.sessionId, + sequence: 0, + sessionId: event.sessionId, + turnId: event.turnId, + type: "turn.started", }, - name: "eve.step", - spanAttributes: { type: SpanTypeAttribute.LLM }, - spanId, - startTime: eventTime(event), - }); - span.log({ - ...(input !== undefined ? { input } : {}), + context, metadata, - }); - - turn.stepsByIndex.set(event.data.stepIndex, { - ...(input !== undefined ? { input } : {}), + ); + const ids = await generateEveIds("turn", event.sessionId, event.turnId); + return { + key, metadata, - metrics: {}, - ...(output !== undefined ? { output } : {}), - reasoning, + rootSpanId: span.rootSpanId, + rowId: ids.rowId, span, - }); + spanId: span.spanId, + }; } - private handleReasoningCompleted( - event: Extract< - EveHandleMessageStreamEvent, - { type: "reasoning.completed" } - >, - ctx: unknown, - ): void { - const sessionId = sessionIdFromContext(ctx); - if (!sessionId) { - return; + private async turnForScope( + scope: EveInstrumentationModelCallStartedEvent["scope"], + context: EveInstrumentationHandlerContext, + ): Promise { + const key = turnKey(scope.sessionId, scope.turnId); + const existing = this.turnsByKey.get(key); + if (existing !== undefined) { + return existing; } - - const reasoning = storeEveReasoning(this.state, sessionId, event); - const step = this.stepForEvent(event, ctx); - if (step) { - step.reasoning = reasoning; - step.output = mergeEveReasoning(step.output, reasoning); + const starting = this.turnsStartingByKey.get(key); + if (starting !== undefined) { + return await starting; } + + const ids = await generateEveIds("turn", scope.sessionId, scope.turnId); + const persistedRootSpanId = readSpanReference( + context.state.get(), + )?.rootSpanId; + const rememberedRootSpanId = eveRootSpanIdsByTurnKey.get(key); + return { + key, + metadata: this.metadata(scope.sessionId), + rootSpanId: + persistedRootSpanId ?? + rememberedRootSpanId ?? + (await deterministicEveId( + "eve:root", + scope.rootSessionId ?? scope.sessionId, + scope.turnId, + )), + rowId: ids.rowId, + span: NOOP_SPAN, + spanId: ids.spanId, + }; } - private handleMessageCompleted( - event: Extract, - ctx: unknown, + private updateTurn( + turn: TurnState, + event: { input?: unknown; output?: unknown }, ): void { - const step = this.stepForEvent(event, ctx); - if (!step) { + currentLogger()?.updateSpan({ + id: turn.rowId, + root_span_id: turn.rootSpanId, + span_id: turn.spanId, + ...event, + }); + } + + private closeOpenModel(attemptId: string, idempotencyKey: string): void { + const openModels = this.openModelsByAttempt.get(attemptId); + if (openModels === undefined) { return; } - - const existingMessage = eveOutputMessage(step.output); - const existingToolCalls = isObject(existingMessage) - ? existingMessage.tool_calls - : undefined; - step.output = mergeEveReasoning( - [ - { - finish_reason: normalizedFinishReason(event.data.finishReason), - index: 0, - message: { - content: event.data.message, - role: "assistant", - ...(Array.isArray(existingToolCalls) - ? { tool_calls: existingToolCalls } - : {}), - }, - }, - ], - step.reasoning, - ); - - const turn = this.turnForEvent(event, ctx); - if (turn && event.data.finishReason !== "tool-calls") { - turn.output = event.data.message; + openModels.delete(idempotencyKey); + if (openModels.size === 0) { + this.openModelsByAttempt.delete(attemptId); } } - private handleResultCompleted( - event: Extract, - ctx: unknown, + private restoreTurnMetricContributions( + turnKey: string, + state: unknown, ): void { - const step = this.stepForEvent(event, ctx); - if (step) { - step.output = mergeEveReasoning( - [ - { - finish_reason: "stop", - index: 0, - message: { - content: event.data.result, - role: "assistant", - }, - }, - ], - step.reasoning, - ); - } - - const turn = this.turnForEvent(event, ctx); - if (turn) { - turn.output = event.data.result; + const persisted = readSpanReference(state)?.turnMetricContributions; + if ( + persisted === undefined || + this.turnMetricContributionsByKey.has(turnKey) + ) { + return; } + this.turnMetricContributionsByKey.set(turnKey, { ...persisted }); } - private async handleActionsRequested( - event: Extract, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - const turn = await this.ensureTurn(event, ctx, hookMetadata); - const sessionId = sessionIdFromContext(ctx); - if (!turn || !sessionId) { - return; + private updateTurnMetrics( + turn: TurnState, + idempotencyKey: string, + metrics: Record, + ): void { + const contributions = { + ...(this.turnMetricContributionsByKey.get(turn.key) ?? {}), + [idempotencyKey]: metrics, + }; + this.turnMetricContributionsByKey.set(turn.key, contributions); + const totals: Record = {}; + for (const contribution of Object.values(contributions)) { + for (const [name, value] of Object.entries(contribution)) { + totals[name] = (totals[name] ?? 0) + value; + } } - - const traceActions = event.data.actions.filter(isTraceableActionRequest); - if (traceActions.length === 0) { + if (Object.keys(totals).length === 0) { return; } - - for (const action of traceActions) { - if (isToolCallAction(action)) { - await this.startRequestedTool(event, turn, sessionId, action); - } else if (isLocalSubagentCallAction(action)) { - await this.startRequestedSubagent(event, turn, sessionId, action); - } - } - - const step = turn.stepsByIndex.get(event.data.stepIndex); - if (!step) { - return; - } - - const toolCallsById = new Map(); - if (Array.isArray(step.output) && isObject(step.output[0])) { - const message = step.output[0]["message"]; - if (isObject(message) && Array.isArray(message["tool_calls"])) { - for (const toolCall of message["tool_calls"]) { - if (isObject(toolCall) && typeof toolCall["id"] === "string") { - toolCallsById.set(toolCall["id"], toolCall); - } - } - } - } - for (const action of traceActions) { - const name = - action.kind === "tool-call" - ? action.toolName - : (action.subagentName ?? action.name ?? "agent"); - toolCallsById.set(action.callId, { - function: { - arguments: JSON.stringify(action.input), - name, - }, - id: action.callId, - type: "function", - }); - } - - step.output = mergeEveReasoning( - [ - { - finish_reason: "tool_calls", - index: 0, - message: { - content: null, - role: "assistant", - tool_calls: [...toolCallsById.values()], - }, - }, - ], - step.reasoning, - ); - } - - private async handleActionResult( - event: Extract, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - if (isToolResult(event.data.result)) { - await this.handleToolResult(event, ctx, event.data.result, hookMetadata); - return; - } - if (isSubagentResult(event.data.result)) { - await this.handleSubagentResult( - event, - ctx, - event.data.result, - hookMetadata, - ); - } - } - - private async handleToolResult( - event: Extract, - ctx: unknown, - result: EveRuntimeToolResultActionResult, - hookMetadata?: Record, - ): Promise { - const sessionId = sessionIdFromContext(ctx); - if (!sessionId) { - return; - } - - const key = toolKey(sessionId, result.callId); - if (this.completedToolKeys.has(key)) { - return; - } - const tool = - this.toolsByCallKey.get(key) ?? - (await this.startSyntheticTool(event, ctx, result, hookMetadata)); - if (!tool) { - return; - } - const failed = - event.data.status === "failed" || - result.isError === true || - event.data.error !== undefined; - tool.span.log({ - ...(failed - ? { - error: actionResultError(event.data.error, result.output), - } - : {}), - metadata: tool.metadata, - output: result.output, + currentLogger()?.updateSpan({ + id: turn.rowId, + metrics: totals, + root_span_id: turn.rootSpanId, + span_id: turn.spanId, }); - - const endTime = eventTime(event); - tool.span.end(endTime === undefined ? undefined : { endTime }); - this.toolsByCallKey.delete(key); - this.completedToolKeys.set(key, true); } - private async handleSubagentCalled( - event: Extract, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - if (event.data.remote?.url) { - return; - } - - const turn = await this.ensureTurn(event, ctx, hookMetadata); - const sessionId = sessionIdFromContext(ctx); - if (!turn || !sessionId) { - return; - } - - const key = toolKey(sessionId, event.data.callId); - const metadata = toolMetadataFromTurn(turn); - const existing = this.toolsByCallKey.get(key); - if (existing) { - existing.metadata = { ...existing.metadata, ...metadata }; - existing.span.log({ metadata: existing.metadata }); - return; - } - if (this.completedToolKeys.has(key)) { - return; - } - - const { rowId: eventId, spanId } = await generateEveIds( - "subagent", - sessionId, - event.data.callId, - ); - const pending = this.toolsByCallKey.get(key); - if (pending || this.completedToolKeys.has(key)) { - if (pending) { - pending.metadata = { ...pending.metadata, ...metadata }; - pending.span.log({ metadata: pending.metadata }); - } - return; - } - const span = await this.startEveChildSpan(turn.span, { + private async resumeModelSpan( + event: + | EveInstrumentationModelCallCompletedEvent + | EveInstrumentationModelCallFailedEvent, + context: EveInstrumentationHandlerContext, + turn: TurnState, + ): Promise { + const ids = await generateEveIds("step", event.idempotencyKey); + return await this.startEveSpan(context, { event: { - id: eventId, - metadata, + id: ids.rowId, + metadata: this.metadata(event.scope.sessionId), }, - name: event.data.toolName ?? event.data.name, - spanAttributes: { type: SpanTypeAttribute.TOOL }, - spanId, - startTime: eventTime(event), - }); - span.log({ metadata }); - this.toolsByCallKey.set(key, { - metadata, - span, - turnKey: turnKey(sessionId, event.data.turnId), + name: "eve.step", + parentSpanIds: { + rootSpanId: turn.rootSpanId, + spanId: turn.spanId, + }, + spanAttributes: { type: SpanTypeAttribute.LLM }, + spanId: ids.spanId, }); } - private async handleSubagentCompleted( - event: Extract, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - const sessionId = sessionIdFromContext(ctx); - if (!sessionId) { - return; - } - - const key = toolKey(sessionId, event.data.callId); - if (this.completedToolKeys.has(key)) { - return; - } - const subagent = - this.toolsByCallKey.get(key) ?? - (await this.startSyntheticSubagent(event, ctx, hookMetadata)); - if (!subagent) { - return; - } - subagent.span.log({ - ...(event.data.status === "failed" - ? { - error: actionResultError(event.data.error, event.data.output), - } - : {}), - metadata: subagent.metadata, - ...(event.data.output !== undefined ? { output: event.data.output } : {}), - }); - const endTime = eventTime(event); - const recordedEndTime = subagent.span.end( - endTime === undefined ? undefined : { endTime }, + private async resumeActionSpan( + event: + | EveInstrumentationActionCompletedEvent + | EveInstrumentationActionFailedEvent, + context: EveInstrumentationHandlerContext, + ): Promise { + const turn = await this.turnForScope(event.scope, context); + const separator = event.idempotencyKey.lastIndexOf(":"); + const callId = + separator < 0 + ? event.idempotencyKey + : event.idempotencyKey.slice(separator + 1); + const ids = await generateEveIds( + "action", + event.scope.sessionId, + event.scope.turnId, + callId, ); - this.state.update((current) => { - const normalized = normalizeEveTraceState(current); - return { - ...normalized, - spanReferences: normalized.spanReferences.map((reference) => - reference.spanId === subagent.span.spanId - ? { ...reference, endTime: recordedEndTime } - : reference, - ), - }; - }); - } - - private async handleSubagentResult( - event: Extract, - ctx: unknown, - result: Extract, - hookMetadata?: Record, - ): Promise { - const sessionId = sessionIdFromContext(ctx); - if (!sessionId) { - return; - } - - const key = toolKey(sessionId, result.callId); - if (this.completedToolKeys.has(key)) { - return; - } - const subagent = - this.toolsByCallKey.get(key) ?? - (await this.startSyntheticSubagentResult( - event, - ctx, - result, - hookMetadata, - )); - if (!subagent) { - return; - } - const isError = - event.data.status === "failed" || - result.isError === true || - event.data.error !== undefined; - subagent.span.log({ - ...(isError - ? { - error: actionResultError(event.data.error, result.output), - } - : {}), - metadata: subagent.metadata, - output: result.output, + return await this.startEveSpan(context, { + event: { id: ids.rowId, metadata: turn.metadata }, + name: "eve.action", + parentSpanIds: { + rootSpanId: turn.rootSpanId, + spanId: turn.spanId, + }, + spanAttributes: { type: SpanTypeAttribute.TOOL }, + spanId: ids.spanId, }); - const endTime = eventTime(event); - subagent.span.end(endTime === undefined ? undefined : { endTime }); - - this.toolsByCallKey.delete(key); - this.completedToolKeys.set(key, true); } - private handleStepCompleted( - event: Extract, - ctx: unknown, - ): void { - const step = this.stepForEvent(event, ctx); - if (!step) { - return; - } - - const usage = event.data.usage; - const inputTokens = - typeof usage?.inputTokens === "number" && - Number.isFinite(usage.inputTokens) && - usage.inputTokens >= 0 - ? usage.inputTokens - : undefined; - const outputTokens = - typeof usage?.outputTokens === "number" && - Number.isFinite(usage.outputTokens) && - usage.outputTokens >= 0 - ? usage.outputTokens - : undefined; - const cacheReadTokens = - typeof usage?.cacheReadTokens === "number" && - Number.isFinite(usage.cacheReadTokens) && - usage.cacheReadTokens >= 0 - ? usage.cacheReadTokens - : undefined; - const cacheWriteTokens = - typeof usage?.cacheWriteTokens === "number" && - Number.isFinite(usage.cacheWriteTokens) && - usage.cacheWriteTokens >= 0 - ? usage.cacheWriteTokens - : undefined; - const costUsd = - typeof usage?.costUsd === "number" && - Number.isFinite(usage.costUsd) && - usage.costUsd >= 0 - ? usage.costUsd - : undefined; - const total = - inputTokens !== undefined && outputTokens !== undefined - ? inputTokens + outputTokens - : undefined; - const metrics = { - ...(inputTokens !== undefined ? { prompt_tokens: inputTokens } : {}), - ...(outputTokens !== undefined - ? { completion_tokens: outputTokens } - : {}), - ...(total !== undefined ? { tokens: total } : {}), - ...(cacheReadTokens !== undefined - ? { prompt_cached_tokens: cacheReadTokens } - : {}), - ...(cacheWriteTokens !== undefined - ? { prompt_cache_creation_tokens: cacheWriteTokens } - : {}), - ...(costUsd !== undefined ? { estimated_cost: costUsd } : {}), + private metadata(sessionId: string): Record { + return { + ...(this.configuredMetadata ?? {}), + "eve.session_id": sessionId, }; - step.metrics = { ...step.metrics, ...metrics }; - const sessionId = sessionIdFromContext(ctx); - if (Array.isArray(step.output) && isObject(step.output[0])) { - const finishReason = step.output[0].finish_reason; - if (typeof finishReason !== "string") { - step.output[0].finish_reason = normalizedFinishReason( - event.data.finishReason, - ); - } - } - step.span.log({ - ...(step.input !== undefined ? { input: step.input } : {}), - metadata: step.metadata, - metrics, - output: step.output, - }); - const endTime = eventTime(event); - step.span.end(endTime === undefined ? undefined : { endTime }); - - const turn = this.turnForEvent(event, ctx); - if (turn) { - for (const [key, value] of Object.entries(metrics)) { - turn.metrics[key] = (turn.metrics[key] ?? 0) + value; - } - turn.stepsByIndex.delete(event.data.stepIndex); - } - this.markStepEnded(event.data.turnId, event.data.stepIndex); - if (sessionId) { - clearStoredEveReasoning( - this.state, - sessionId, - event.data.turnId, - event.data.stepIndex, - ); - } - } - - private handleStepFailed( - event: Extract, - ctx: unknown, - ): void { - const step = this.stepForEvent(event, ctx); - if (step) { - step.span.log({ - error: errorFromMessage( - event.data.message, - event.data.code, - event.data.details, - ), - }); - const endTime = eventTime(event); - step.span.end(endTime === undefined ? undefined : { endTime }); - } - - const turn = this.turnForEvent(event, ctx); - turn?.stepsByIndex.delete(event.data.stepIndex); - this.markStepEnded(event.data.turnId, event.data.stepIndex); - const sessionId = sessionIdFromContext(ctx); - if (sessionId) { - clearStoredEveReasoning( - this.state, - sessionId, - event.data.turnId, - event.data.stepIndex, - ); - } } +} - private handleTurnCompleted( - event: Extract, - ctx: unknown, - ): void { - const turn = this.turnForEvent(event, ctx); - if (!turn) { - return; - } - - this.finalizeTurn(turn, { - endTime: eventTime(event), - }); +function readSpanReference(value: unknown): EveSpanReference | undefined { + if (!isObject(value)) { + return undefined; } - - private handleTurnFailed( - event: Extract, - ctx: unknown, - ): void { - const turn = this.turnForEvent(event, ctx); - if (!turn) { - return; - } - - this.finalizeTurn(turn, { - endTime: eventTime(event), - error: errorFromMessage( - event.data.message, - event.data.code, - event.data.details, - ), - }); + const exported = value["exported"]; + const rootSpanId = value["rootSpanId"]; + const spanId = value["spanId"]; + if ( + typeof exported !== "string" || + typeof rootSpanId !== "string" || + typeof spanId !== "string" + ) { + return undefined; } - - private handleSessionFailed( - event: Extract, - ctx: unknown, - ): void { - const sessionId = event.data.sessionId || sessionIdFromContext(ctx); - if (!sessionId) { - return; - } - const error = errorFromMessage( - event.data.message, - event.data.code, - event.data.details, - ); - for (const [key, turn] of this.turnsByKey) { - if (!key.startsWith(`${sessionId}:`)) { - continue; - } - this.finalizeTurn(turn, { - endTime: eventTime(event), - error, - }); - } - - for (const [key, tool] of this.toolsByCallKey) { - if (key.startsWith(`${sessionId}:`)) { - const endTime = eventTime(event); - if (!tool.endedByTurn) { - tool.span.log({ metadata: tool.metadata }); - tool.span.end(endTime === undefined ? undefined : { endTime }); - tool.endedByTurn = true; + return { + exported, + rootSpanId, + spanId, + ...(isObject(value["turnMetricContributions"]) + ? { + turnMetricContributions: numericMetricContributions( + value["turnMetricContributions"], + ), } - } - } - } - - private handleSessionCompleted( - event: Extract, - ctx: unknown, - ): void { - const sessionId = sessionIdFromContext(ctx); - if (!sessionId) { - return; - } - for (const [key, turn] of this.turnsByKey) { - if (!key.startsWith(`${sessionId}:`)) { - continue; - } - this.finalizeTurn(turn, { - endTime: eventTime(event), - }); - } + : {}), + }; +} - for (const [key, tool] of this.toolsByCallKey) { - if (key.startsWith(`${sessionId}:`) && !tool.endedByTurn) { - const endTime = eventTime(event); - tool.span.log({ metadata: tool.metadata }); - tool.span.end(endTime === undefined ? undefined : { endTime }); - tool.endedByTurn = true; - } - } +function capturedModelInput( + input: EveInstrumentationModelInput | undefined, +): unknown[] | undefined { + if (input === undefined) { + return undefined; } - - private async ensureTurn( - event: Extract< - EveHandleMessageStreamEvent, - { - data: { readonly sequence: number; readonly turnId: string }; + const value: unknown[] = []; + if (typeof input.instructions === "string") { + value.push({ content: input.instructions, role: "system" }); + } else if (Array.isArray(input.instructions)) { + for (const instruction of input.instructions) { + const projected = projectedModelMessage(instruction); + if (projected?.role === "system") { + value.push(projected); } - >, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - const sessionId = sessionIdFromContext(ctx); - if (!sessionId) { - return undefined; } - - const key = turnKey(sessionId, event.data.turnId); - const existing = this.turnsByKey.get(key); - if (existing) { - return existing; + } else if (isObject(input.instructions)) { + const instruction = projectedModelMessage(input.instructions); + if (instruction?.role === "system") { + value.push(instruction); } - - const metadata = { - ...readEveTraceState(this.state).metadata, - ...(hookMetadata ?? {}), - "eve.session_id": sessionId, - }; - const span = await this.startTurnSpan(sessionId, event, ctx, metadata); - span.log({ metadata }); - const state = { - key, - metadata, - metrics: {}, - sessionId, - span, - stepsByIndex: new Map(), - turnId: event.data.turnId, - }; - this.turnsByKey.set(key, state); - return state; } - - private async startRequestedTool( - event: Extract, - turn: TurnState, - sessionId: string, - action: EveRuntimeToolCallActionRequest, - ): Promise { - const key = toolKey(sessionId, action.callId); - if (this.toolsByCallKey.has(key) || this.completedToolKeys.has(key)) { - return; + for (const message of input.messages) { + const projected = projectedModelMessage(message); + if (projected !== undefined) { + value.push(projected); } - - const metadata = toolMetadataFromTurn(turn); - const { rowId: eventId, spanId } = await generateEveIds( - "tool", - sessionId, - event.data.turnId, - action.callId, - ); - if (this.toolsByCallKey.has(key) || this.completedToolKeys.has(key)) { - return; - } - const span = await this.startEveChildSpan(turn.span, { - event: { - id: eventId, - input: action.input, - metadata, - }, - name: action.toolName, - spanAttributes: { type: SpanTypeAttribute.TOOL }, - spanId, - startTime: eventTime(event), - }); - span.log({ input: action.input, metadata }); - this.toolsByCallKey.set(key, { - metadata, - span, - turnKey: turnKey(sessionId, event.data.turnId), - }); } + return value; +} - private async startRequestedSubagent( - event: Extract, - turn: TurnState, - sessionId: string, - action: Extract, - ): Promise { - const key = toolKey(sessionId, action.callId); - if (this.toolsByCallKey.has(key) || this.completedToolKeys.has(key)) { - return; - } - - const name = action.subagentName ?? action.name ?? "agent"; - const metadata = toolMetadataFromTurn(turn); - const { rowId: eventId, spanId } = await generateEveIds( - "subagent", - sessionId, - action.callId, - ); - if (this.toolsByCallKey.has(key) || this.completedToolKeys.has(key)) { - return; - } - const span = await this.startEveChildSpan(turn.span, { - event: { - id: eventId, - input: action.input, - metadata, - }, - name, - spanAttributes: { type: SpanTypeAttribute.TOOL }, - spanId, - startTime: eventTime(event), - }); - span.log({ input: action.input, metadata }); - this.toolsByCallKey.set(key, { - metadata, - span, - turnKey: turnKey(sessionId, event.data.turnId), - }); - } - - private async startSyntheticTool( - event: Extract, - ctx: unknown, - result: EveRuntimeToolResultActionResult, - hookMetadata?: Record, - ): Promise { - const turn = await this.ensureTurn(event, ctx, hookMetadata); - const sessionId = sessionIdFromContext(ctx); - if (!turn || !sessionId) { - return undefined; - } - - const metadata = toolMetadataFromTurn(turn); - const { rowId: eventId, spanId } = await generateEveIds( - "tool", - sessionId, - event.data.turnId, - result.callId, - ); - const existing = this.toolsByCallKey.get(toolKey(sessionId, result.callId)); - if (existing) { - return existing; - } - const span = await this.startEveChildSpan(turn.span, { - event: { - id: eventId, - metadata, - }, - name: result.toolName, - spanAttributes: { type: SpanTypeAttribute.TOOL }, - spanId, - startTime: eventTime(event), - }); - span.log({ metadata }); - const state = { - metadata, - span, - turnKey: turnKey(sessionId, event.data.turnId), - }; - this.toolsByCallKey.set(toolKey(sessionId, result.callId), state); - return state; - } - - private async startSyntheticSubagent( - event: Extract, - ctx: unknown, - hookMetadata?: Record, - ): Promise { - const turn = await this.ensureTurn(event, ctx, hookMetadata); - const sessionId = sessionIdFromContext(ctx); - if (!turn || !sessionId) { - return undefined; - } - - const metadata = toolMetadataFromTurn(turn); - const { rowId: eventId, spanId } = await generateEveIds( - "subagent", - sessionId, - event.data.callId, - ); - const existing = this.toolsByCallKey.get( - toolKey(sessionId, event.data.callId), - ); - if (existing) { - return existing; - } - const span = await this.startEveChildSpan(turn.span, { - event: { - id: eventId, - metadata, - }, - name: event.data.subagentName, - spanAttributes: { type: SpanTypeAttribute.TOOL }, - spanId, - startTime: eventTime(event), - }); - span.log({ metadata }); - const state = { - metadata, - span, - turnKey: turnKey(sessionId, event.data.turnId), - }; - this.toolsByCallKey.set(toolKey(sessionId, event.data.callId), state); - return state; - } - - private async startSyntheticSubagentResult( - event: Extract, - ctx: unknown, - result: Extract, - hookMetadata?: Record, - ): Promise { - const turn = await this.ensureTurn(event, ctx, hookMetadata); - const sessionId = sessionIdFromContext(ctx); - if (!turn || !sessionId) { - return undefined; - } - - const metadata = toolMetadataFromTurn(turn); - const { rowId: eventId, spanId } = await generateEveIds( - "subagent", - sessionId, - result.callId, - ); - const existing = this.toolsByCallKey.get(toolKey(sessionId, result.callId)); - if (existing) { - return existing; - } - const span = await this.startEveChildSpan(turn.span, { - event: { - id: eventId, - metadata, - }, - name: result.subagentName, - spanAttributes: { type: SpanTypeAttribute.TOOL }, - spanId, - startTime: eventTime(event), - }); - span.log({ metadata }); - const state = { - metadata, - span, - turnKey: turnKey(sessionId, event.data.turnId), - }; - this.toolsByCallKey.set(toolKey(sessionId, result.callId), state); - return state; +function projectedModelMessage( + value: unknown, +): { content: unknown; role: string } | undefined { + if (!isObject(value)) { + return undefined; } - - private async startTurnSpan( - sessionId: string, - event: Extract< - EveHandleMessageStreamEvent, - { data: { readonly sequence: number; readonly turnId: string } } - >, - ctx: unknown, - metadata: Record, - ): Promise { - const session = isObject(ctx) ? ctx["session"] : undefined; - const parent = isObject(session) ? session["parent"] : undefined; - const parentTurn = isObject(parent) ? parent["turn"] : undefined; - const parentLineage = - isObject(parent) && - typeof parent["callId"] === "string" && - typeof parent["sessionId"] === "string" && - isObject(parentTurn) && - typeof parentTurn["id"] === "string" - ? { - callId: parent["callId"], - sessionId: parent["sessionId"], - turnId: parentTurn["id"], - } - : undefined; - const [{ rowId: eventId, spanId }, rootSpanId, parentSpanId] = - await Promise.all([ - generateEveIds("turn", sessionId, event.data.turnId), - deterministicEveId( - "eve:root", - parentLineage?.sessionId ?? sessionId, - parentLineage?.turnId ?? event.data.turnId, - ), - parentLineage - ? deterministicEveId( - "eve:subagent", - parentLineage.sessionId, - parentLineage.callId, - ) - : Promise.resolve(undefined), - ]); - - return await this.startEveSpan({ - event: { - id: eventId, - metadata, - }, - name: "eve.turn", - parentSpanIds: parentSpanId - ? { rootSpanId, spanId: parentSpanId } - : { parentSpanIds: [], rootSpanId }, - spanAttributes: { type: SpanTypeAttribute.TASK }, - spanId, - startTime: eventTime(event), - }); + const role = value["role"]; + if ( + role !== "system" && + role !== "user" && + role !== "assistant" && + role !== "tool" + ) { + return undefined; } - - private turnForEvent( - event: Extract< - EveHandleMessageStreamEvent, - { data: { readonly turnId: string } } - >, - ctx: unknown, - ): TurnState | undefined { - const sessionId = sessionIdFromContext(ctx); - return sessionId - ? this.turnsByKey.get(turnKey(sessionId, event.data.turnId)) - : undefined; + const content = value["content"]; + if (typeof content === "string") { + return { content, role }; } - - private stepForEvent( - event: Extract< - EveHandleMessageStreamEvent, - { data: { readonly stepIndex: number; readonly turnId: string } } - >, - ctx: unknown, - ): StepState | undefined { - return this.turnForEvent(event, ctx)?.stepsByIndex.get( - event.data.stepIndex, - ); + if (!Array.isArray(content)) { + return undefined; } - - private finalizeTurn( - turn: TurnState, - args: { endTime: number | undefined; error?: Error }, - ): void { - const { endTime } = args; - for (const step of turn.stepsByIndex.values()) { - step.span.log({ - ...(step.input !== undefined ? { input: step.input } : {}), - metadata: step.metadata, - metrics: step.metrics, - output: step.output, - }); - step.span.end(endTime === undefined ? undefined : { endTime }); - } - turn.stepsByIndex.clear(); - - for (const tool of this.toolsByCallKey.values()) { - if (tool.turnKey !== turn.key) { - continue; + const projected: unknown[] = []; + for (const part of content) { + if (!isObject(part) || typeof part["type"] !== "string") { + continue; + } + switch (part["type"]) { + case "text": + case "reasoning": + if (typeof part["text"] === "string") { + projected.push({ text: part["text"], type: part["type"] }); + } + break; + case "image": { + if (part["image"] === undefined) { + break; + } + const mediaType = + typeof part["mediaType"] === "string" + ? part["mediaType"] + : "image/png"; + projected.push({ + image: mediaAttachment(part["image"], mediaType, undefined), + ...(typeof part["mediaType"] === "string" ? { mediaType } : {}), + type: "image", + }); + break; } - if (tool.endedByTurn) { - continue; + case "file": { + const mediaType = + typeof part["mediaType"] === "string" + ? part["mediaType"] + : "application/octet-stream"; + const data = + part["data"] === undefined + ? undefined + : mediaAttachment( + part["data"], + mediaType, + typeof part["filename"] === "string" + ? part["filename"] + : undefined, + ); + projected.push({ + ...(data === undefined ? {} : { data }), + ...(typeof part["filename"] === "string" + ? { filename: part["filename"] } + : {}), + mediaType, + type: "file", + }); + break; } - tool.span.log({ metadata: tool.metadata }); - tool.span.end(endTime === undefined ? undefined : { endTime }); - tool.endedByTurn = true; + case "tool-call": + if ( + typeof part["toolCallId"] === "string" && + typeof part["toolName"] === "string" + ) { + projected.push({ + input: part["input"], + toolCallId: part["toolCallId"], + toolName: part["toolName"], + type: "tool-call", + }); + } + break; + case "tool-result": + if ( + typeof part["toolCallId"] === "string" && + typeof part["toolName"] === "string" + ) { + projected.push({ + output: projectedToolOutput(part["output"]), + toolCallId: part["toolCallId"], + toolName: part["toolName"], + type: "tool-result", + }); + } + break; + case "tool-approval-response": + if ( + typeof part["approvalId"] === "string" && + typeof part["approved"] === "boolean" + ) { + projected.push({ + approvalId: part["approvalId"], + approved: part["approved"], + ...(typeof part["reason"] === "string" + ? { reason: part["reason"] } + : {}), + type: "tool-approval-response", + }); + } + break; + default: + break; } + } + return { content: projected, role }; +} - if (args.error) { - turn.span.log({ error: args.error }); - } else { - turn.span.log({ - metadata: turn.metadata, - metrics: turn.metrics, - output: turn.output, - }); - } - turn.span.end(endTime === undefined ? undefined : { endTime }); - this.turnsByKey.delete(turn.key); - this.state.update((current) => { - const normalized = normalizeEveTraceState(current); +function projectedToolOutput(value: unknown): unknown { + if (!isObject(value)) { + return value; + } + if (typeof value["type"] !== "string") { + return undefined; + } + switch (value["type"]) { + case "text": + case "json": + case "error-text": + case "error-json": + return { type: value["type"], value: value["value"] }; + case "execution-denied": return { - ...normalized, - reasoningBlocks: normalized.reasoningBlocks.filter( - (entry) => - !entry.key.startsWith(`${turn.sessionId}\0${turn.turnId}\0`), - ), - stepStarts: normalized.stepStarts.filter( - (entry) => entry.turnId !== turn.turnId, - ), + ...(typeof value["reason"] === "string" + ? { reason: value["reason"] } + : {}), + type: "execution-denied", }; - }); - } - - private cleanupSession(sessionId: string): void { - const keyPrefix = `${sessionId}:`; - for (const key of this.turnsByKey.keys()) { - if (key.startsWith(keyPrefix)) { - this.turnsByKey.delete(key); - } - } - for (const key of this.toolsByCallKey.keys()) { - if (key.startsWith(keyPrefix)) { - this.toolsByCallKey.delete(key); + case "content": { + if (!Array.isArray(value["value"])) { + return { type: "content", value: [] }; } - } - for (const key of this.completedToolKeys.keys()) { - if (key.startsWith(keyPrefix)) { - this.completedToolKeys.delete(key); + const content: unknown[] = []; + for (const part of value["value"]) { + if (!isObject(part) || typeof part["type"] !== "string") { + continue; + } + if (part["type"] === "text" && typeof part["text"] === "string") { + content.push({ text: part["text"], type: "text" }); + } else if ( + (part["type"] === "file-data" || part["type"] === "image-data") && + part["data"] !== undefined && + typeof part["mediaType"] === "string" + ) { + const data = mediaAttachment( + part["data"], + part["mediaType"], + typeof part["filename"] === "string" ? part["filename"] : undefined, + ); + content.push({ + ...(data === undefined ? {} : { data }), + ...(typeof part["filename"] === "string" + ? { filename: part["filename"] } + : {}), + mediaType: part["mediaType"], + type: part["type"], + }); + } else if ( + (part["type"] === "file-url" || part["type"] === "image-url") && + typeof part["url"] === "string" + ) { + content.push({ type: part["type"], url: part["url"] }); + } } + return { + type: "content", + value: content, + }; } - this.state.update(() => emptyEveTraceState()); - } - - private async flushInstrumentation(): Promise { - try { - await flush(); - return true; - } catch (error) { - debugLogger.warn("Error in Eve flush instrumentation:", error); - return false; - } - } -} - -function emptyEveTraceState(): EveTraceState { - return { - llmInputs: [], - metadata: {}, - reasoningBlocks: [], - spanReferences: [], - stepStarts: [], - }; -} - -function normalizeEveTraceState(state: unknown): EveTraceState { - if (!isObject(state)) { - return emptyEveTraceState(); + default: + return undefined; } - const metadata = isObject(state["metadata"]) ? state["metadata"] : {}; - const spanReferences = Array.isArray(state["spanReferences"]) - ? state["spanReferences"] - .flatMap((entry): EveTraceState["spanReferences"] => { - if (!isObject(entry)) { - return []; - } - const exported = entry["exported"]; - const endTime = entry["endTime"]; - const rootSpanId = entry["rootSpanId"]; - const rowId = entry["rowId"]; - const spanId = entry["spanId"]; - const startEvent = entry["startEvent"]; - const startEventCreated = isObject(startEvent) - ? startEvent["created"] - : undefined; - const startEventMetrics = isObject(startEvent) - ? startEvent["metrics"] - : undefined; - const startEventSpanAttributes = isObject(startEvent) - ? startEvent["span_attributes"] - : undefined; - const startEventSpanParents = isObject(startEvent) - ? startEvent["span_parents"] - : undefined; - const normalizedStartEvent = - typeof startEventCreated === "string" && - isObject(startEventMetrics) && - typeof startEventMetrics["start"] === "number" && - Number.isFinite(startEventMetrics["start"]) && - isObject(startEventSpanAttributes) && - Array.isArray(startEventSpanParents) && - startEventSpanParents.every( - (parent): parent is string => typeof parent === "string", - ) - ? { - created: startEventCreated, - metrics: { start: startEventMetrics["start"] }, - span_attributes: { ...startEventSpanAttributes }, - span_parents: [...startEventSpanParents], - } - : undefined; - return typeof exported === "string" && - typeof rootSpanId === "string" && - typeof rowId === "string" && - typeof spanId === "string" - ? [ - { - ...(typeof endTime === "number" && Number.isFinite(endTime) - ? { endTime } - : {}), - exported, - rootSpanId, - rowId, - spanId, - ...(normalizedStartEvent - ? { startEvent: normalizedStartEvent } - : {}), - }, - ] - : []; - }) - .slice(-MAX_STORED_SPAN_REFERENCES) - : []; - const llmInputs = Array.isArray(state["llmInputs"]) - ? state["llmInputs"] - .flatMap((entry): EveTraceState["llmInputs"] => { - if (!isObject(entry)) { - return []; - } - const key = entry["key"]; - const input = entry["input"]; - return typeof key === "string" && isCapturedModelInput(input) - ? [{ input, key }] - : []; - }) - .slice(-MAX_STORED_LLM_INPUTS) - : []; - const reasoningBlocks = Array.isArray(state["reasoningBlocks"]) - ? state["reasoningBlocks"] - .flatMap((entry): EveTraceState["reasoningBlocks"] => { - if (!isObject(entry)) { - return []; - } - const content = entry["content"]; - const eventAt = entry["eventAt"]; - const key = entry["key"]; - return typeof content === "string" && - (eventAt === undefined || typeof eventAt === "string") && - typeof key === "string" - ? [ - { - content, - ...(typeof eventAt === "string" ? { eventAt } : {}), - key, - }, - ] - : []; - }) - .slice(-MAX_STORED_REASONING_BLOCKS) - : []; - const stepStarts = Array.isArray(state["stepStarts"]) - ? state["stepStarts"] - .flatMap((entry): EveTraceState["stepStarts"] => { - if (!isObject(entry)) { - return []; - } - const ordinal = entry["ordinal"]; - const open = entry["open"]; - const stepIndex = entry["stepIndex"]; - const turnId = entry["turnId"]; - return typeof ordinal === "number" && - Number.isInteger(ordinal) && - ordinal >= 0 && - typeof open === "boolean" && - typeof stepIndex === "number" && - Number.isInteger(stepIndex) && - typeof turnId === "string" - ? [{ open, ordinal, stepIndex, turnId }] - : []; - }) - .slice(-MAX_STORED_STEP_STARTS) - : []; - return { - llmInputs, - metadata: { ...metadata }, - reasoningBlocks, - spanReferences, - stepStarts, - }; } -function readEveTraceState( - state: EveStateHandle, -): EveTraceState { - try { - return normalizeEveTraceState(state.get()); - } catch { - return emptyEveTraceState(); +function mediaAttachment( + data: unknown, + contentType: string, + filename: string | undefined, +): unknown { + const blob = convertDataToBlob(data, contentType); + if (blob === null) { + return data; } -} - -function storeEveReasoning( - state: EveStateHandle, - sessionId: string, - event: Extract, -): readonly EveReasoningBlock[] { - const eventAt = event.meta?.at; - const key = llmInputKey(sessionId, event.data.turnId, event.data.stepIndex); - let stored: readonly EveReasoningBlock[] = []; - state.update((current) => { - const normalized = normalizeEveTraceState(current); - const alreadyStored = normalized.reasoningBlocks.some( - (entry) => - entry.content === event.data.reasoning && - entry.eventAt === eventAt && - entry.key === key, - ); - const reasoningBlocks = alreadyStored - ? normalized.reasoningBlocks - : [ - ...normalized.reasoningBlocks, - { - content: event.data.reasoning, - ...(eventAt ? { eventAt } : {}), - key, - }, - ].slice(-MAX_STORED_REASONING_BLOCKS); - stored = reasoningBlocks.flatMap((entry) => - entry.key === key - ? [ - { - content: entry.content, - ...(entry.eventAt ? { eventAt: entry.eventAt } : {}), - }, - ] - : [], - ); - return alreadyStored ? normalized : { ...normalized, reasoningBlocks }; + return new Attachment({ + contentType, + data: blob, + filename: + filename ?? `attachment.${getExtensionFromMediaType(contentType)}`, }); - return stored; } -function readStoredEveReasoning( - state: EveStateHandle, - sessionId: string, - turnId: string, - stepIndex: number, -): readonly EveReasoningBlock[] { - const key = llmInputKey(sessionId, turnId, stepIndex); - return readEveTraceState(state).reasoningBlocks.flatMap((entry) => - entry.key === key - ? [ - { - content: entry.content, - ...(entry.eventAt ? { eventAt: entry.eventAt } : {}), - }, - ] - : [], - ); +function initialTurnInput(input: readonly unknown[]): unknown[] | undefined { + for (let index = input.length - 1; index >= 0; index--) { + const message = input[index]; + if (isObject(message) && message["role"] === "user") { + return [message]; + } + } + return undefined; } -function clearStoredEveReasoning( - state: EveStateHandle, - sessionId: string, - turnId: string, - stepIndex: number, -): void { - const key = llmInputKey(sessionId, turnId, stepIndex); - state.update((current) => { - const normalized = normalizeEveTraceState(current); - return { - ...normalized, - reasoningBlocks: normalized.reasoningBlocks.filter( - (entry) => entry.key !== key, - ), - }; +function modelOutput( + finishReason: string, + content: readonly EveInstrumentationContentPart[] | undefined, +): unknown { + const parts = content ?? []; + const text = parts + .filter( + ( + part, + ): part is Extract => + part.type === "text", + ) + .map((part) => part.text) + .join(""); + const reasoning = parts + .filter( + ( + part, + ): part is Extract< + EveInstrumentationContentPart, + { type: "reasoning" } + > => part.type === "reasoning", + ) + .map((part) => ({ content: part.text })); + const toolCalls = parts.flatMap((part) => { + if (part.type !== "tool-call") { + return []; + } + return [ + { + function: { + arguments: jsonString(part.input), + name: part.toolName, + }, + id: part.callId, + type: "function", + }, + ]; }); -} - -function eveOutputMessage(output: unknown): unknown { - return Array.isArray(output) && isObject(output[0]) - ? output[0]["message"] - : undefined; -} - -function mergeEveReasoning( - output: unknown, - reasoning: readonly { content: string }[], -): unknown | undefined { - if (reasoning.length === 0) { - return output; - } - - const choice = Array.isArray(output) && isObject(output[0]) ? output[0] : {}; - const message = isObject(choice["message"]) ? choice["message"] : {}; return [ { - ...choice, - index: typeof choice["index"] === "number" ? choice["index"] : 0, + finish_reason: normalizedFinishReason(finishReason), + index: 0, message: { - ...message, - content: "content" in message ? message["content"] : null, - reasoning: reasoning.map((block) => ({ content: block.content })), - role: - typeof message["role"] === "string" ? message["role"] : "assistant", + content: text.length === 0 ? null : text, + ...(reasoning.length === 0 ? {} : { reasoning }), + role: "assistant", + ...(toolCalls.length === 0 ? {} : { tool_calls: toolCalls }), }, }, ]; } -function captureEveModelInput( - state: EveStateHandle, - input: EveInstrumentationStepStartedEventInput, -): void { - const sessionId = input.session.id; - const turnId = input.turn.id; - const stepIndex = input.step.index; - const captured = capturedModelInput(input.modelInput); - if (!captured) { - return; - } - - const key = llmInputKey(sessionId, turnId, stepIndex); - state.update((current) => { - const normalized = normalizeEveTraceState(current); - const llmInputs = [...normalized.llmInputs, { input: captured, key }]; - return { - ...normalized, - llmInputs: llmInputs.slice(-MAX_STORED_LLM_INPUTS), - }; - }); -} - -function consumeCapturedEveModelInput( - state: EveStateHandle, - sessionId: string, - turnId: string, - stepIndex: number, -): CapturedEveModelInput | undefined { - try { - const key = llmInputKey(sessionId, turnId, stepIndex); - let input: CapturedEveModelInput | undefined; - state.update((current) => { - const normalized = normalizeEveTraceState(current); - const index = normalized.llmInputs.findIndex( - (candidate) => candidate.key === key, - ); - if (index < 0) { - return normalized; - } - input = normalized.llmInputs[index]?.input; - return { - ...normalized, - llmInputs: normalized.llmInputs.filter( - (_, candidateIndex) => candidateIndex !== index, - ), - }; - }); - return input; - } catch (error) { - debugLogger.warn("Error in Eve LLM input consumption:", error); - return undefined; - } +function finalText( + content: readonly EveInstrumentationContentPart[] | undefined, +): string | undefined { + const text = (content ?? []) + .filter( + ( + part, + ): part is Extract => + part.type === "text", + ) + .map((part) => part.text) + .join(""); + return text.length === 0 ? undefined : text; } -function capturedModelInput( - modelInput: EveInstrumentationModelInput, -): CapturedEveModelInput | undefined { - const { instructions, messages } = modelInput; - const value: CapturedEveModelMessage[] = []; - if (typeof instructions === "string") { - value.push({ content: instructions, role: "system" }); - } else if (instructions) { - value.push(...instructions.map(capturedEveModelMessage)); - } - value.push(...messages.map(capturedEveModelMessage)); - +function jsonString(value: unknown): string { try { - const cloned: unknown = JSON.parse(JSON.stringify(value)); - if (!Array.isArray(cloned)) { - return undefined; - } - return cloned; + return JSON.stringify(value) ?? "null"; } catch { - return undefined; + return "null"; } } -function capturedEveModelMessage( - message: EveModelMessage, -): CapturedEveModelMessage { - const { content, role } = message; - if (typeof content === "string") { - return { content, role }; +function normalizedFinishReason(finishReason: string): string { + if (finishReason === "content-filter") { + return "content_filter"; } - return { content: content.map(capturedEveModelContentPart), role }; -} - -function capturedEveModelContentPart( - part: EveModelMessageContentPart, -): Record { - switch (part.type) { - case "text": - case "reasoning": - return { text: part.text, type: part.type }; - case "image": - return { - image: part.image, - ...(part.mediaType !== undefined ? { mediaType: part.mediaType } : {}), - type: "image", - }; - case "file": - case "reasoning-file": - return { - data: part.data, - ...(part.type === "file" && part.filename !== undefined - ? { filename: part.filename } - : {}), - mediaType: part.mediaType, - type: part.type, - }; - case "custom": - return { - ...("kind" in part ? { kind: part.kind } : {}), - type: "custom", - }; - case "tool-call": - return { - input: part.input, - ...(part.providerExecuted !== undefined - ? { providerExecuted: part.providerExecuted } - : {}), - toolCallId: part.toolCallId, - toolName: part.toolName, - type: "tool-call", - }; - case "tool-result": { - const output = part.output; - let capturedOutput: Record; - switch (output.type) { - case "text": - case "error-text": - capturedOutput = { type: output.type, value: output.value }; - break; - case "json": - case "error-json": - capturedOutput = { type: output.type, value: output.value }; - break; - case "execution-denied": - capturedOutput = { - ...(output.reason !== undefined ? { reason: output.reason } : {}), - type: "execution-denied", - }; - break; - case "content": - capturedOutput = { - type: "content", - value: output.value.map(capturedEveModelContentPart), - }; - break; - } - return { - output: capturedOutput, - toolCallId: part.toolCallId, - toolName: part.toolName, - type: "tool-result", - }; - } - case "tool-approval-request": - return { - approvalId: part.approvalId, - ...(part.isAutomatic !== undefined - ? { isAutomatic: part.isAutomatic } - : {}), - ...(part.signature !== undefined ? { signature: part.signature } : {}), - toolCallId: part.toolCallId, - type: "tool-approval-request", - }; - case "tool-approval-response": - return { - approvalId: part.approvalId, - approved: part.approved, - ...(part.providerExecuted !== undefined - ? { providerExecuted: part.providerExecuted } - : {}), - ...(part.reason !== undefined ? { reason: part.reason } : {}), - type: "tool-approval-response", - }; - case "file-data": - case "image-data": - return { - data: part.data, - ...(part.type === "file-data" && part.filename !== undefined - ? { filename: part.filename } - : {}), - mediaType: part.mediaType, - type: part.type, - }; - case "file-url": - case "image-url": - return { - ...(part.type === "file-url" && part.mediaType !== undefined - ? { mediaType: part.mediaType } - : {}), - type: part.type, - url: part.url, - }; - case "file-id": - case "image-file-id": - return { fileId: part.fileId, type: part.type }; - case "file-reference": - case "image-file-reference": - return { - providerReference: part.providerReference, - type: part.type, - }; + if (finishReason === "tool-calls") { + return "tool_calls"; } + return finishReason; } -function isCapturedModelInput(input: unknown): input is CapturedEveModelInput { - return ( - Array.isArray(input) && - input.every( - (message) => - isObject(message) && - (message["role"] === "system" || - message["role"] === "user" || - message["role"] === "assistant" || - message["role"] === "tool") && - (typeof message["content"] === "string" || - (Array.isArray(message["content"]) && - message["content"].every(isObject))), - ) +function usageMetrics(usage: EveInstrumentationUsage): Record { + const inputTokens = validMetric(usage.inputTokens); + const outputTokens = validMetric(usage.outputTokens); + const cacheReadTokens = validMetric(usage.inputTokenDetails?.cacheReadTokens); + const cacheWriteTokens = validMetric( + usage.inputTokenDetails?.cacheWriteTokens, ); + return { + ...(inputTokens === undefined ? {} : { prompt_tokens: inputTokens }), + ...(outputTokens === undefined ? {} : { completion_tokens: outputTokens }), + ...(inputTokens === undefined || outputTokens === undefined + ? {} + : { tokens: inputTokens + outputTokens }), + ...(cacheReadTokens === undefined + ? {} + : { prompt_cached_tokens: cacheReadTokens }), + ...(cacheWriteTokens === undefined + ? {} + : { prompt_cache_creation_tokens: cacheWriteTokens }), + }; } -function llmInputKey( - sessionId: string, - turnId: string, - stepIndex: number, -): string { - return `${sessionId}\0${turnId}\0${stepIndex}`; +function validMetric(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) && value >= 0 + ? value + : undefined; } -function modelMetadataFromRuntime(runtime: unknown): Record { - if (!isObject(runtime)) { - return {}; +function numericMetrics( + value: Record, +): Record { + const metrics: Record = {}; + for (const name of [ + "completion_tokens", + "prompt_cache_creation_tokens", + "prompt_cached_tokens", + "prompt_tokens", + "tokens", + ]) { + const metric = validMetric(value[name]); + if (metric !== undefined) { + metrics[name] = metric; + } } - const modelId = runtime["modelId"]; - return typeof modelId === "string" && !modelId.trim().startsWith("dynamic:") - ? modelMetadataFromModelId(modelId) - : {}; + return metrics; } -function modelMetadataFromModelId(modelId: string): Record { - const normalized = modelId.trim(); - if (!normalized) { - return {}; - } - - const slashIndex = normalized.indexOf("/"); - if (slashIndex > 0 && slashIndex < normalized.length - 1) { - return { - model: normalized.slice(slashIndex + 1), - provider: normalized.slice(0, slashIndex), - }; +function numericMetricContributions( + value: Record, +): Record> { + const contributions: Record> = {}; + for (const [idempotencyKey, contribution] of Object.entries(value)) { + if (isObject(contribution)) { + contributions[idempotencyKey] = numericMetrics(contribution); + } } + return contributions; +} +function modelMetadata( + modelId: string, + providerId: string, +): Record { + const provider = providerId.split(".")[0]?.trim(); + const model = modelId.trim(); return { - model: normalized, + ...(model.length === 0 ? {} : { model }), + ...(provider === undefined || provider.length === 0 ? {} : { provider }), }; } -function sessionIdFromContext(ctx: unknown): string | undefined { - if (!isObject(ctx)) { - return undefined; +function toError(error: unknown, fallback: string): Error { + if (error instanceof Error) { + return error; } - const session = ctx["session"]; - if (!isObject(session)) { - return undefined; + if (typeof error === "string" && error.length > 0) { + return new Error(error); } - const id = session["id"]; - return typeof id === "string" ? id : undefined; -} - -function toolMetadataFromTurn(turn: TurnState): Record { - const { model: _model, provider: _provider, ...metadata } = turn.metadata; - return metadata; -} - -function isToolCallAction( - action: unknown, -): action is EveRuntimeToolCallActionRequest { - return ( - isObject(action) && - action["kind"] === "tool-call" && - typeof action["callId"] === "string" && - typeof action["toolName"] === "string" && - isObject(action["input"]) - ); -} - -function isLocalSubagentCallAction( - action: unknown, -): action is Extract { - return ( - isObject(action) && - action["kind"] === "subagent-call" && - typeof action["callId"] === "string" && - isObject(action["input"]) - ); -} - -function isTraceableActionRequest( - action: unknown, -): action is - | EveRuntimeToolCallActionRequest - | Extract { - return isToolCallAction(action) || isLocalSubagentCallAction(action); -} - -function isToolResult( - result: unknown, -): result is EveRuntimeToolResultActionResult { - return ( - isObject(result) && - result["kind"] === "tool-result" && - typeof result["callId"] === "string" && - typeof result["toolName"] === "string" - ); -} - -function isSubagentResult( - result: unknown, -): result is Extract { - return ( - isObject(result) && - result["kind"] === "subagent-result" && - typeof result["callId"] === "string" && - typeof result["subagentName"] === "string" - ); -} - -function normalizedFinishReason( - finishReason: EveAssistantStepFinishReason, -): string { - switch (finishReason) { - case "content-filter": - return "content_filter"; - case "tool-calls": - return "tool_calls"; - default: - return finishReason; - } -} - -function errorFromMessage( - message: string, - code: string, - details?: unknown, -): Error { - const error = new Error(`${code}: ${message}`); - if (details !== undefined) { - error.cause = details; - } - return error; -} - -function actionResultError( - error: EveActionResultError | undefined, - output: unknown, -): Error { - if (error) { - return errorFromMessage(error.message, error.code); + if (isObject(error) && typeof error["message"] === "string") { + const code = typeof error["code"] === "string" ? error["code"] + ": " : ""; + const result = new Error(code + error["message"]); + result.cause = error; + return result; } - const result = new Error("Eve action failed"); - if (output !== undefined) { - result.cause = output; + const result = new Error(fallback); + if (error !== undefined) { + result.cause = error; } return result; } -function eventTime(event: { - readonly meta?: { readonly at: string }; -}): number | undefined { - if (!event.meta?.at) { - return undefined; - } - const timestamp = Date.parse(event.meta.at); - return Number.isFinite(timestamp) ? timestamp / 1000 : undefined; +function actionEndArgs( + acceptedAtMs: number | undefined, +): { endTime: number } | undefined { + return typeof acceptedAtMs === "number" && Number.isFinite(acceptedAtMs) + ? { endTime: acceptedAtMs / 1000 } + : undefined; } function turnKey(sessionId: string, turnId: string): string { - return `${sessionId}:${turnId}`; -} - -function toolKey(sessionId: string, callId: string): string { - return `${sessionId}:${callId}`; + return sessionId + ":" + turnId; } async function generateEveIds( - kind: EveEntityKind, + kind: "action" | "step" | "turn", ...parts: string[] ): Promise<{ rowId: string; spanId: string }> { const [rowId, spanId] = await Promise.all([ - deterministicEveId(`eve:row:${kind}`, ...parts), - deterministicEveId(`eve:${kind}`, ...parts), + deterministicEveId("eve:row:" + kind, ...parts), + deterministicEveId("eve:" + kind, ...parts), ]); return { rowId, spanId }; } async function deterministicEveId(...parts: string[]): Promise { const data = new TextEncoder().encode( - parts.map((part) => `${part.length}:${part}`).join("\0"), + parts.map((part) => part.length + ":" + part).join("\0"), ); const digest = await globalThis.crypto.subtle.digest("SHA-256", data); const bytes = Array.from(new Uint8Array(digest, 0, 16)); const hex = bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); - return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; + return ( + hex.slice(0, 8) + + "-" + + hex.slice(8, 12) + + "-" + + hex.slice(12, 16) + + "-" + + hex.slice(16, 20) + + "-" + + hex.slice(20) + ); } diff --git a/js/src/vendor-sdk-types/eve.ts b/js/src/vendor-sdk-types/eve.ts index 1910b3a87..5499fceb9 100644 --- a/js/src/vendor-sdk-types/eve.ts +++ b/js/src/vendor-sdk-types/eve.ts @@ -1,8 +1,8 @@ /** - * Vendored types for eve's authored hook APIs. + * Vendored types for eve's instrumentation provider API, introduced in 0.34.0. * * Keep this surface intentionally narrow. These types are not exported to SDK - * users and should only cover fields we read, correlate, or log. + * users and should only cover fields the Braintrust provider reads or returns. */ export type EveJsonValue = @@ -13,499 +13,197 @@ export type EveJsonValue = | EveJsonValue[] | { readonly [key: string]: EveJsonValue }; -export type EveJsonObject = { readonly [key: string]: EveJsonValue }; - -export interface EveHookContext { - readonly session: { - readonly id: string; - readonly parent?: { - readonly callId?: string; - readonly sessionId?: string; - readonly turn?: { - readonly id?: string; - }; - }; - }; +export interface EveInstrumentationState { + get(): EveJsonValue | undefined; + set(value: EveJsonValue | undefined): void; } -export type EveAssistantStepFinishReason = - | "content-filter" - | "error" - | "length" - | "other" - | "stop" - | "tool-calls"; +export interface EveInstrumentationHandlerContext { + readonly state: EveInstrumentationState; +} -export interface EveStreamEventMeta { - readonly at: string; +export interface EveInstrumentationSetupContext { + readonly agentName: string; + readonly environment?: "development" | "preview" | "production"; + readonly evaluation?: { readonly runId: string }; + readonly frameworkVersion?: string; } -export interface EveRuntimeToolCallActionRequest { - readonly callId: string; - readonly input: EveJsonObject; - readonly kind: "tool-call"; - readonly toolName: string; +export interface EveInstrumentationAttemptScope { + readonly attemptId: string; + readonly attemptIndex: number; + readonly rootSessionId?: string; + readonly sessionId: string; + readonly stepIndex: number; + readonly turnId: string; } -export interface EveRuntimeToolResultActionResult { +export interface EveInstrumentationParentLineage { readonly callId: string; - readonly isError?: boolean; - readonly kind: "tool-result"; - readonly output: EveJsonValue; - readonly toolName: string; + readonly sessionId: string; + readonly turnId: string; } -export type EveRuntimeActionRequest = - | EveRuntimeToolCallActionRequest - | { - readonly callId: string; - readonly input?: EveJsonObject; - readonly kind: "load-skill" | "remote-agent-call"; - readonly name?: string; - } - | { - readonly callId: string; - readonly input: EveJsonObject; - readonly kind: "subagent-call"; - readonly name?: string; - readonly subagentName?: string; - }; - -export type EveRuntimeActionResult = - | EveRuntimeToolResultActionResult - | { - readonly callId: string; - readonly isError?: boolean; - readonly kind: "load-skill-result"; - readonly output?: EveJsonValue; - readonly name?: string; - } - | { - readonly callId: string; - readonly isError?: boolean; - readonly kind: "subagent-result"; - readonly output?: EveJsonValue; - readonly subagentName?: string; - }; +export interface EveInstrumentationTraceContext { + readonly spanId: string; + readonly traceFlags: number; + readonly traceId: string; +} -export type EveActionResultStatus = "completed" | "failed" | "rejected"; +export interface EveInstrumentationUsage { + readonly inputTokenDetails?: { + readonly cacheReadTokens?: number; + readonly cacheWriteTokens?: number; + }; + readonly inputTokens?: number; + readonly outputTokens?: number; +} -export interface EveActionResultError { - readonly code: string; - readonly message: string; +export interface EveInstrumentationModelInput { + readonly instructions?: unknown; + readonly messages: readonly unknown[]; } -export type EveHandleMessageStreamEvent = - | { - readonly data: { - readonly invocation?: unknown; - readonly runtime?: { - readonly agentId: string; - readonly agentName?: string; - readonly eveVersion: string; - readonly modelId: string; - }; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "session.started"; - } +export type EveInstrumentationContentPart = | { - readonly data: { - readonly sequence: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "turn.started"; + readonly text: string; + readonly type: "text"; } | { - readonly data: { - readonly sequence: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "turn.completed"; + readonly text: string; + readonly type: "reasoning"; } | { - readonly data: { - readonly message: string; - readonly sequence: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "message.received"; - } - | { - readonly data: { - readonly finishReason: EveAssistantStepFinishReason; - readonly message: string | null; - readonly sequence: number; - readonly stepIndex: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "message.completed"; - } - | { - readonly data: { - readonly reasoning: string; - readonly sequence: number; - readonly stepIndex: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "reasoning.completed"; - } - | { - readonly data: { - readonly result: EveJsonValue; - readonly sequence: number; - readonly stepIndex: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "result.completed"; - } - | { - readonly data: { - readonly sequence: number; - readonly stepIndex: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "step.started"; - } - | { - readonly data: { - readonly finishReason: EveAssistantStepFinishReason; - readonly providerMetadata?: { - readonly gateway?: { - readonly generationId?: string; - }; - }; - readonly sequence: number; - readonly stepIndex: number; - readonly turnId: string; - readonly usage?: { - readonly cacheReadTokens?: number; - readonly cacheWriteTokens?: number; - readonly costUsd?: number; - readonly inputTokens?: number; - readonly outputTokens?: number; - }; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "step.completed"; - } - | { - readonly data: { - readonly code: string; - readonly details?: EveJsonObject; - readonly message: string; - readonly sequence: number; - readonly stepIndex: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "step.failed"; - } - | { - readonly data: { - readonly actions: readonly EveRuntimeActionRequest[]; - readonly sequence: number; - readonly stepIndex: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "actions.requested"; - } - | { - readonly data: { - readonly error?: EveActionResultError; - readonly result: EveRuntimeActionResult; - readonly sequence: number; - readonly stepIndex: number; - readonly status: EveActionResultStatus; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "action.result"; - } - | { - readonly data: { - readonly callId: string; - readonly childSessionId: string; - readonly name: string; - readonly remote?: { - readonly url?: string; - }; - readonly sequence: number; - readonly toolName?: string; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "subagent.called"; - } - | { - readonly data: { - readonly callId: string; - readonly error?: EveActionResultError; - readonly output?: EveJsonValue; - readonly sequence: number; - readonly status?: EveActionResultStatus; - readonly subagentName: string; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "subagent.completed"; - } - | { - readonly data: { - readonly code: string; - readonly details?: EveJsonObject; - readonly message: string; - readonly sequence: number; - readonly turnId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "turn.failed"; - } - | { - readonly data: { - readonly code: string; - readonly details?: EveJsonObject; - readonly message: string; - readonly sessionId: string; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "session.failed"; + readonly callId: string; + readonly input: unknown; + readonly toolName: string; + readonly type: "tool-call"; } | { - readonly data: { - readonly wait: "next-user-message"; - }; - readonly meta?: EveStreamEventMeta; - readonly type: "session.waiting"; + readonly type: "tool-result"; } | { - readonly meta?: EveStreamEventMeta; - readonly type: "session.completed"; + readonly type: "tool-error"; }; -export interface EveHookDefinition { - readonly events?: { - readonly "*"?: ( - event: EveHandleMessageStreamEvent, - ctx: EveHookContext, - ) => void | Promise; - readonly [eventType: string]: - | (( - event: EveHandleMessageStreamEvent, - ctx: EveHookContext, - ) => void | Promise) - | undefined; - }; +export interface EveInstrumentationTurnStartedEvent { + readonly idempotencyKey: string; + readonly parentLineage?: EveInstrumentationParentLineage; + readonly parentTraceContext?: EveInstrumentationTraceContext; + readonly rootSessionId: string; + readonly sequence: number; + readonly sessionId: string; + readonly turnId: string; + readonly type: "turn.started"; } -export interface EveInstrumentationSetupContext { - readonly agentName: string; +export interface EveInstrumentationTurnSettledEvent { + readonly idempotencyKey: string; + readonly sessionId: string; + readonly turnId: string; + readonly type: "turn.cancelled" | "turn.completed"; } -type EveTextPart = { - readonly text: string; - readonly type: "text"; -}; - -type EveImagePart = { - readonly image: unknown; - readonly mediaType?: string; - readonly type: "image"; -}; - -type EveFilePart = { - readonly data: unknown; - readonly filename?: string; - readonly mediaType: string; - readonly type: "file"; -}; - -type EveReasoningPart = { - readonly text: string; - readonly type: "reasoning"; -}; - -type EveReasoningFilePart = { - readonly data: unknown; - readonly mediaType: string; - readonly type: "reasoning-file"; -}; - -type EveCustomPart = { - readonly kind: `${string}.${string}`; - readonly type: "custom"; -}; - -type EveToolCallPart = { - readonly input: unknown; - readonly providerExecuted?: boolean; - readonly toolCallId: string; - readonly toolName: string; - readonly type: "tool-call"; -}; - -type EveToolResultContentPart = - | EveTextPart - | EveFilePart - | { - readonly data: string; - readonly filename?: string; - readonly mediaType: string; - readonly type: "file-data"; - } - | { - readonly mediaType?: string; - readonly type: "file-url"; - readonly url: string; - } - | { - readonly fileId: string | Readonly>; - readonly type: "file-id" | "image-file-id"; - } - | { - readonly providerReference: Readonly>; - readonly type: "file-reference" | "image-file-reference"; - } - | { - readonly data: string; - readonly mediaType: string; - readonly type: "image-data"; - } - | { - readonly type: "image-url"; - readonly url: string; - } - | { readonly type: "custom" }; - -type EveToolResultOutput = - | { - readonly type: "text" | "error-text"; - readonly value: string; - } - | { - readonly type: "json" | "error-json"; - readonly value: EveJsonValue; - } - | { - readonly reason?: string; - readonly type: "execution-denied"; - } - | { - readonly type: "content"; - readonly value: readonly EveToolResultContentPart[]; - }; +export interface EveInstrumentationTurnFailedEvent { + readonly error?: unknown; + readonly idempotencyKey: string; + readonly sessionId: string; + readonly turnId: string; + readonly type: "turn.failed"; +} -type EveToolResultPart = { - readonly output: EveToolResultOutput; - readonly toolCallId: string; - readonly toolName: string; - readonly type: "tool-result"; -}; +export interface EveInstrumentationModelCallStartedEvent { + readonly idempotencyKey: string; + readonly input?: EveInstrumentationModelInput; + readonly model: { + readonly modelId: string; + readonly provider: string; + }; + readonly scope: EveInstrumentationAttemptScope; + readonly type: "model.call.started"; +} -type EveToolApprovalRequest = { - readonly approvalId: string; - readonly isAutomatic?: boolean; - readonly signature?: string; - readonly toolCallId: string; - readonly type: "tool-approval-request"; -}; +export interface EveInstrumentationModelCallCompletedEvent { + readonly content?: readonly EveInstrumentationContentPart[]; + readonly finishReason: string; + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; + readonly type: "model.call.completed"; + readonly usage: EveInstrumentationUsage; +} -type EveToolApprovalResponse = { - readonly approvalId: string; - readonly approved: boolean; - readonly providerExecuted?: boolean; - readonly reason?: string; - readonly type: "tool-approval-response"; -}; +export interface EveInstrumentationModelCallFailedEvent { + readonly error?: unknown; + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; + readonly type: "model.call.failed"; +} -export type EveModelMessageContentPart = - | EveTextPart - | EveImagePart - | EveFilePart - | EveReasoningPart - | EveReasoningFilePart - | EveCustomPart - | EveToolCallPart - | EveToolResultPart - | EveToolApprovalRequest - | EveToolApprovalResponse - | EveToolResultContentPart; +export interface EveInstrumentationStepAttemptCompletedEvent { + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; + readonly type: "step.attempt.completed"; +} -export type EveSystemModelMessage = { - readonly content: string; - readonly role: "system"; -}; +export interface EveInstrumentationStepAttemptFailedEvent { + readonly error?: unknown; + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; + readonly type: "step.attempt.failed"; +} -export type EveModelMessage = - | EveSystemModelMessage - | { - readonly content: - | string - | readonly (EveTextPart | EveImagePart | EveFilePart)[]; - readonly role: "user"; - } - | { - readonly content: - | string - | readonly ( - | EveTextPart - | EveCustomPart - | EveFilePart - | EveReasoningPart - | EveReasoningFilePart - | EveToolCallPart - | EveToolResultPart - | EveToolApprovalRequest - )[]; - readonly role: "assistant"; - } - | { - readonly content: readonly ( - | EveToolResultPart - | EveToolApprovalResponse - )[]; - readonly role: "tool"; - }; +export interface EveInstrumentationActionStartedEvent { + readonly callId: string; + readonly idempotencyKey: string; + readonly input?: unknown; + readonly name: string; + readonly scope: EveInstrumentationAttemptScope; + readonly type: "action.started"; +} -export interface EveInstrumentationModelInput { - readonly instructions?: string | readonly EveSystemModelMessage[]; - readonly messages: readonly EveModelMessage[]; +export interface EveInstrumentationActionCompletedEvent { + readonly acceptedAtMs?: number; + readonly idempotencyKey: string; + readonly output: + | { readonly output?: unknown; readonly type: "result" } + | { readonly error?: unknown; readonly type: "error" }; + readonly scope: EveInstrumentationAttemptScope; + readonly type: "action.completed"; } -export interface EveInstrumentationStepStartedEventInput { - readonly modelInput: EveInstrumentationModelInput; - readonly session: { - readonly id: string; - }; - readonly step: { - readonly index: number; - }; - readonly turn: { - readonly id: string; - readonly sequence: number; - }; +export interface EveInstrumentationActionFailedEvent { + readonly acceptedAtMs?: number; + readonly error?: unknown; + readonly errorCode?: string; + readonly idempotencyKey: string; + readonly outcome: "abandoned" | "cancelled" | "failed" | "rejected"; + readonly scope: EveInstrumentationAttemptScope; + readonly type: "action.failed"; } +type EveInstrumentationHandler = ( + event: TEvent, + context: EveInstrumentationHandlerContext, +) => void | PromiseLike; + export interface EveInstrumentationDefinition { - readonly events?: { - readonly "step.started"?: ( - input: EveInstrumentationStepStartedEventInput, - ) => void | { readonly runtimeContext?: EveJsonObject }; - readonly [eventType: string]: - | (( - input: EveInstrumentationStepStartedEventInput, - ) => void | { readonly runtimeContext?: EveJsonObject }) - | undefined; + readonly capture: "content"; + readonly events: { + readonly "action.completed": EveInstrumentationHandler; + readonly "action.failed": EveInstrumentationHandler; + readonly "action.started": EveInstrumentationHandler; + readonly "model.call.completed": EveInstrumentationHandler; + readonly "model.call.failed": EveInstrumentationHandler; + readonly "model.call.started": EveInstrumentationHandler; + readonly "step.attempt.completed": EveInstrumentationHandler; + readonly "step.attempt.failed": EveInstrumentationHandler; + readonly "turn.cancelled": EveInstrumentationHandler; + readonly "turn.completed": EveInstrumentationHandler; + readonly "turn.failed": EveInstrumentationHandler; + readonly "turn.started": EveInstrumentationHandler; }; - readonly recordInputs?: boolean; - readonly recordOutputs?: boolean; - readonly setup?: (context: EveInstrumentationSetupContext) => void; + readonly flush: () => Promise; + readonly setup?: ( + context: EveInstrumentationSetupContext, + ) => void | PromiseLike; } diff --git a/js/src/wrappers/attachment-utils.ts b/js/src/wrappers/attachment-utils.ts index 7fb2b96f5..df7544fce 100644 --- a/js/src/wrappers/attachment-utils.ts +++ b/js/src/wrappers/attachment-utils.ts @@ -242,6 +242,26 @@ export function processInputAttachments(input: any): any { } } + // AI SDK tool output content format + if ( + (node.type === "image-data" || node.type === "file-data") && + node.data + ) { + const mediaType = node.mediaType || "application/octet-stream"; + const filename = + node.filename || + `output_${node.type === "image-data" ? "image" : "file"}_${attachmentIndex}.${getExtensionFromMediaType(mediaType)}`; + const attachment = toAttachment(node.data, mediaType, filename); + + if (attachment) { + attachmentIndex++; + return { + ...node, + data: attachment, + }; + } + } + const processed: Record = {}; for (const [key, value] of Object.entries(node)) { processed[key] = processNode(value); From 4cd71d131a9f71e75698fade337294dfbf4df58d Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 26 Aug 2026 20:08:53 +0200 Subject: [PATCH 05/19] ref!: Remove Zod derived types from public SDK declarations (#2404) ref: [SDK-226](https://linear.app/braintrustdata/issue/SDK-226) Replace Zod-inferred public types with explicit TypeScript types to reduce declaration size and downstream compile cost. --- .changeset/remove-zod-derived-types.md | 5 + AGENTS.md | 15 +++ js/src/eval-parameters.ts | 35 +++--- js/src/exports.ts | 7 +- js/src/framework-types.ts | 2 +- js/src/framework.ts | 14 +-- js/src/framework2.ts | 24 ++-- js/src/functions/invoke.ts | 14 +-- js/src/functions/stream.ts | 91 ++++++++------ js/src/gitutil.ts | 8 +- js/src/graph-framework.ts | 14 +-- js/src/isomorph.ts | 8 +- js/src/logger.ts | 114 ++++++++++++------ js/src/prompt-schemas.ts | 59 ++++++--- js/src/public-types.test.ts | 69 +++++++++++ js/src/sandbox.ts | 2 +- .../api-compatibility.test.ts | 50 ++++++++ js/util/git_fields.ts | 2 +- js/util/object.ts | 6 +- js/util/span_identifier_v3.ts | 35 +++++- js/util/span_identifier_v4.ts | 9 +- 21 files changed, 416 insertions(+), 167 deletions(-) create mode 100644 .changeset/remove-zod-derived-types.md create mode 100644 js/src/public-types.test.ts diff --git a/.changeset/remove-zod-derived-types.md b/.changeset/remove-zod-derived-types.md new file mode 100644 index 000000000..c741bede7 --- /dev/null +++ b/.changeset/remove-zod-derived-types.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref!: Remove Zod derived types from public SDK declarations diff --git a/AGENTS.md b/AGENTS.md index 6338e8830..1b62e3529 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,21 @@ mise install # Install toolchain and dependencies pnpm run build # Build all workspace packages (from repo root) ``` +## Public TypeScript APIs + +Do not derive SDK-owned public TypeScript types from Zod schemas (for example, +with `z.infer`, `z.input`, `z.output`, or equivalent schema-derived aliases). +Define public API types explicitly with interfaces, type aliases, or generated +plain types. Generic APIs may still infer types from caller-provided schemas. +When exporting a runtime validator, give it a compact public type such as +`z.ZodType` and test that the validator and public type stay in sync. + +Zod-derived public declarations can expand into large schema implementation +graphs. Those declarations are expensive for downstream TypeScript consumers to +parse, instantiate, and type-check, increasing compile time, declaration size, +and memory usage. They also expose validation-library implementation details as +part of the SDK's API surface. + ## Instrumentation Use the normal Orchestrion config plus plugin/channel path by default. Special-case source patches should be rare exceptions only when the target SDK cannot be instrumented through the standard transformer path, and the reason should be documented next to the patch. diff --git a/js/src/eval-parameters.ts b/js/src/eval-parameters.ts index 1154fcfb8..bc15dc105 100644 --- a/js/src/eval-parameters.ts +++ b/js/src/eval-parameters.ts @@ -2,30 +2,25 @@ import { z } from "zod/v3"; import Ajv from "ajv"; import { Prompt, RemoteEvalParameters } from "./logger"; import { - promptDefinitionWithToolsSchema, promptDefinitionToPromptData, + type PromptDefinitionWithTools, } from "./prompt-schemas"; import { PromptData as promptDataSchema } from "./generated_types"; -// Schema for evaluation parameters -export const evalParametersSchema = z.record( - z.string(), - z.union([ - z.object({ - type: z.literal("prompt"), - default: promptDefinitionWithToolsSchema.optional(), - description: z.string().optional(), - }), - z.object({ - type: z.literal("model"), - default: z.string().optional(), - description: z.string().optional(), - }), - z.instanceof(z.ZodType), // For Zod schemas - ]), -); - -export type EvalParameters = z.infer; +export type EvalParameters = Record< + string, + | { + type: "prompt"; + default?: PromptDefinitionWithTools; + description?: string; + } + | { + type: "model"; + default?: string; + description?: string; + } + | z.ZodTypeAny +>; // Type helper to infer the type of a parameter value type InferParameterValue = T extends { type: "prompt" } diff --git a/js/src/exports.ts b/js/src/exports.ts index 9ed496ceb..e8286a959 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -1,3 +1,7 @@ +import type { z } from "zod/v3"; +import { AttachmentReference as attachmentReferenceSchema } from "./generated_types"; +import type { AttachmentReferenceType } from "./generated_plain_types"; + export type { AnyDataset, AttachmentParams, @@ -322,7 +326,8 @@ export type { export { addAzureBlobHeaders, LazyValue } from "./util"; -export { AttachmentReference } from "./generated_types"; +export const AttachmentReference: z.ZodType = + attachmentReferenceSchema; export type { EvalParameters } from "./eval-parameters"; diff --git a/js/src/framework-types.ts b/js/src/framework-types.ts index b5183cc7d..0f9d45c26 100644 --- a/js/src/framework-types.ts +++ b/js/src/framework-types.ts @@ -1,4 +1,4 @@ -import { type IfExistsType as IfExists } from "./generated_types"; +import type { IfExistsType as IfExists } from "./generated_plain_types"; export type GenericFunction = | ((input: Input) => Output) diff --git a/js/src/framework.ts b/js/src/framework.ts index 9a463533b..540b95dc6 100644 --- a/js/src/framework.ts +++ b/js/src/framework.ts @@ -7,13 +7,13 @@ import { SpanTypeAttribute, spanObjectTypeV3ToTypedString, } from "../util/index"; -import { - type GitMetadataSettingsType as GitMetadataSettings, - ObjectReference as ObjectReferenceSchema, - type ObjectReferenceType as ObjectReference, - type RepoInfoType as RepoInfo, - type SSEProgressEventDataType as SSEProgressEventData, -} from "./generated_types"; +import { ObjectReference as ObjectReferenceSchema } from "./generated_types"; +import type { + GitMetadataSettingsType as GitMetadataSettings, + ObjectReferenceType as ObjectReference, + RepoInfoType as RepoInfo, + SSEProgressEventDataType as SSEProgressEventData, +} from "./generated_plain_types"; import { queue } from "async"; import iso from "./isomorph"; diff --git a/js/src/framework2.ts b/js/src/framework2.ts index f9220b6e1..ab4ee52c5 100644 --- a/js/src/framework2.ts +++ b/js/src/framework2.ts @@ -3,17 +3,17 @@ import type { Trace } from "./trace"; import iso from "./isomorph"; import { slugify } from "../util/string_util"; import { z } from "zod/v3"; -import { - type FunctionTypeEnumType as FunctionType, - type IfExistsType as IfExists, - type SavedFunctionIdType as SavedFunctionId, - type PromptBlockDataType as PromptBlockData, - type PromptDataType as PromptData, - type ToolFunctionDefinitionType as ToolFunctionDefinition, - FunctionData as functionDataSchema, - Project as projectSchema, - type ExtendedSavedFunctionIdType as ExtendedSavedFunctionId, -} from "./generated_types"; +import { Project as projectSchema } from "./generated_types"; +import type { + FunctionTypeEnumType as FunctionType, + IfExistsType as IfExists, + SavedFunctionIdType as SavedFunctionId, + PromptBlockDataType as PromptBlockData, + PromptDataType as PromptData, + ToolFunctionDefinitionType as ToolFunctionDefinition, + ExtendedSavedFunctionIdType as ExtendedSavedFunctionId, + FunctionDataType, +} from "./generated_plain_types"; import { loadPrettyXact, TransactionId } from "../util/index"; import { _internalGetGlobalState, @@ -782,7 +782,7 @@ interface FunctionEvent { name: string; description: string; prompt_data?: PromptData; - function_data: z.infer; + function_data: FunctionDataType; function_type?: FunctionType; if_exists?: IfExists; tags?: string[]; diff --git a/js/src/functions/invoke.ts b/js/src/functions/invoke.ts index f7bbb35f5..82fd40de4 100644 --- a/js/src/functions/invoke.ts +++ b/js/src/functions/invoke.ts @@ -1,10 +1,10 @@ -import { - FunctionId as functionIdSchema, - type InvokeFunctionType as InvokeFunctionRequest, - type ChatCompletionMessageParamType as Message, - type StreamingModeType as StreamingMode, - type FunctionTypeEnumType as FunctionType, -} from "../generated_types"; +import { FunctionId as functionIdSchema } from "../generated_types"; +import type { + InvokeFunctionType as InvokeFunctionRequest, + ChatCompletionMessageParamType as Message, + StreamingModeType as StreamingMode, + FunctionTypeEnumType as FunctionType, +} from "../generated_plain_types"; import { _internalGetGlobalState, BraintrustState, diff --git a/js/src/functions/stream.ts b/js/src/functions/stream.ts index 300895075..392e6b337 100644 --- a/js/src/functions/stream.ts +++ b/js/src/functions/stream.ts @@ -1,9 +1,13 @@ import { - type CallEventType as CallEventSchema, CallEvent as callEventSchema, SSEConsoleEventData as sseConsoleEventDataSchema, SSEProgressEventData as sseProgressEventDataSchema, } from "../generated_types"; +import type { + CallEventType as CallEvent, + SSEConsoleEventDataType, + SSEProgressEventDataType, +} from "../generated_plain_types"; import { createParser, EventSourceParser, @@ -12,46 +16,55 @@ import { } from "eventsource-parser"; import { z } from "zod/v3"; -export const braintrustStreamChunkSchema = z.union([ - z.object({ - type: z.literal("text_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("reasoning_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("json_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("error"), - data: z.string(), - }), - z.object({ - type: z.literal("console"), - data: sseConsoleEventDataSchema, - }), - z.object({ - type: z.literal("progress"), - data: sseProgressEventDataSchema, - }), - z.object({ - type: z.literal("start"), - data: z.string(), - }), - z.object({ - type: z.literal("done"), - data: z.string(), - }), -]); - /** * A chunk of data from a Braintrust stream. Each chunk type matches * an SSE event type. */ -export type BraintrustStreamChunk = z.infer; +export type BraintrustStreamChunk = + | { type: "text_delta"; data: string } + | { type: "reasoning_delta"; data: string } + | { type: "json_delta"; data: string } + | { type: "error"; data: string } + | { type: "console"; data: SSEConsoleEventDataType } + | { type: "progress"; data: SSEProgressEventDataType } + | { type: "start"; data: string } + | { type: "done"; data: string }; + +export const braintrustStreamChunkSchema: z.ZodType = + z.union([ + z.object({ + type: z.literal("text_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("reasoning_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("json_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("error"), + data: z.string(), + }), + z.object({ + type: z.literal("console"), + data: sseConsoleEventDataSchema, + }), + z.object({ + type: z.literal("progress"), + data: sseProgressEventDataSchema, + }), + z.object({ + type: z.literal("start"), + data: z.string(), + }), + z.object({ + type: z.literal("done"), + data: z.string(), + }), + ]); /** * A Braintrust stream. This is a wrapper around a ReadableStream of `BraintrustStreamChunk`, @@ -163,7 +176,7 @@ export class BraintrustStream { return this.memoizedFinalValue; } - static parseRawEvent(event: CallEventSchema): BraintrustStreamChunk { + static parseRawEvent(event: CallEvent): BraintrustStreamChunk { switch (event.event) { case "text_delta": return { @@ -212,7 +225,7 @@ export class BraintrustStream { } } - static serializeRawEvent(event: BraintrustStreamChunk): CallEventSchema { + static serializeRawEvent(event: BraintrustStreamChunk): CallEvent { switch (event.type) { case "text_delta": return { diff --git a/js/src/gitutil.ts b/js/src/gitutil.ts index 8e318ac98..9a687ebb3 100644 --- a/js/src/gitutil.ts +++ b/js/src/gitutil.ts @@ -1,7 +1,7 @@ -import { - type GitMetadataSettingsType as GitMetadataSettings, - type RepoInfoType as RepoInfo, -} from "./generated_types"; +import type { + GitMetadataSettingsType as GitMetadataSettings, + RepoInfoType as RepoInfo, +} from "./generated_plain_types"; import { debugLogger } from "./debug-logger"; import { runGitCommand } from "./git-command"; diff --git a/js/src/graph-framework.ts b/js/src/graph-framework.ts index f60ff3fba..5d6766da6 100644 --- a/js/src/graph-framework.ts +++ b/js/src/graph-framework.ts @@ -1,11 +1,11 @@ import { newId, Prompt } from "./logger"; -import { - type FunctionIdType as FunctionId, - type GraphDataType as GraphData, - type GraphNodeType as GraphNode, - type GraphEdgeType as GraphEdge, - type PromptBlockDataType as PromptBlockData, -} from "./generated_types"; +import type { + FunctionIdType as FunctionId, + GraphDataType as GraphData, + GraphNodeType as GraphNode, + GraphEdgeType as GraphEdge, + PromptBlockDataType as PromptBlockData, +} from "./generated_plain_types"; export interface BuildContext { getFunctionId(functionObj: unknown): Promise; diff --git a/js/src/isomorph.ts b/js/src/isomorph.ts index d23ddcc7e..0b500bcec 100644 --- a/js/src/isomorph.ts +++ b/js/src/isomorph.ts @@ -1,7 +1,7 @@ -import { - type GitMetadataSettingsType as GitMetadataSettings, - type RepoInfoType as RepoInfo, -} from "./generated_types"; +import type { + GitMetadataSettingsType as GitMetadataSettings, + RepoInfoType as RepoInfo, +} from "./generated_plain_types"; import { newGlobalTracingChannel, type GlobalHookAsyncLocalStorage, diff --git a/js/src/logger.ts b/js/src/logger.ts index 9733c3030..fcf88d9a4 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -66,37 +66,38 @@ import { getObjValueByPath, } from "./util"; import { - type AnyModelParamsType as AnyModelParam, AttachmentReference as attachmentReferenceSchema, - type AttachmentReferenceType as AttachmentReference, BraintrustAttachmentReference as BraintrustAttachmentReferenceSchema, - type BraintrustAttachmentReferenceType as BraintrustAttachmentReference, BraintrustModelParams as braintrustModelParamsSchema, ChatCompletionTool as chatCompletionToolSchema, - type ChatCompletionToolType as ChatCompletionTool, ExternalAttachmentReference as ExternalAttachmentReferenceSchema, - type ExternalAttachmentReferenceType as ExternalAttachmentReference, - type ModelParamsType as ModelParams, ResponseFormatJsonSchema as responseFormatJsonSchemaSchema, AttachmentStatus as attachmentStatusSchema, - type AttachmentStatusType as AttachmentStatus, GitMetadataSettings as gitMetadataSettingsSchema, - type GitMetadataSettingsType as GitMetadataSettings, - type ChatCompletionMessageParamType as Message, - type ChatCompletionOpenAIMessageParamType as OpenAIMessage, DatasetSnapshot as datasetSnapshotSchema, - type DatasetSnapshotType as DatasetSnapshot, PromptData as promptDataSchema, - type PromptDataType as PromptData, Prompt as promptSchema, - type PromptType as PromptRow, - type PromptSessionEventType as PromptSessionEvent, - type RepoInfoType as RepoInfo, - type ObjectReferenceType as ObjectReference, - type PromptBlockDataType as PromptBlockData, - type ResponseFormatJsonSchemaType as ResponseFormatJsonSchema, - type ObjectReferenceType, } from "./generated_types"; +import type { + AnyModelParamsType as AnyModelParam, + AttachmentReferenceType as AttachmentReference, + BraintrustAttachmentReferenceType as BraintrustAttachmentReference, + ChatCompletionToolType as ChatCompletionTool, + ExternalAttachmentReferenceType as ExternalAttachmentReference, + ModelParamsType as ModelParams, + AttachmentStatusType as AttachmentStatus, + GitMetadataSettingsType as GitMetadataSettings, + ChatCompletionMessageParamType as Message, + ChatCompletionOpenAIMessageParamType as OpenAIMessage, + DatasetSnapshotType as DatasetSnapshot, + PromptDataType as PromptData, + PromptType as PromptRow, + PromptSessionEventType as PromptSessionEvent, + RepoInfoType as RepoInfo, + ObjectReferenceType as ObjectReference, + PromptBlockDataType as PromptBlockData, + ResponseFormatJsonSchemaType as ResponseFormatJsonSchema, +} from "./generated_plain_types"; const BRAINTRUST_ATTACHMENT = BraintrustAttachmentReferenceSchema.shape.type.value; @@ -120,16 +121,21 @@ const datasetRestorePreviewResultSchema = z.object({ rows_to_restore: z.number(), rows_to_delete: z.number(), }); -export type DatasetRestorePreviewResult = z.infer< - typeof datasetRestorePreviewResultSchema ->; +export type DatasetRestorePreviewResult = { + rows_to_restore: number; + rows_to_delete: number; +}; const datasetRestoreResultSchema = z.object({ xact_id: z.string().nullable(), rows_restored: z.number(), rows_deleted: z.number(), }); -export type DatasetRestoreResult = z.infer; +export type DatasetRestoreResult = { + xact_id: string | null; + rows_restored: number; + rows_deleted: number; +}; const parametersRowSchema = z.object({ id: z.string().uuid(), @@ -148,7 +154,21 @@ const parametersRowSchema = z.object({ .union([z.object({}).partial().passthrough(), z.null()]) .optional(), }); -type ParametersRow = z.infer; +type ParametersRow = { + id: string; + _xact_id: string; + project_id: string; + name: string; + slug: string; + description?: string | null; + function_type: "parameters"; + function_data: { + type: "parameters"; + data?: Record; + __schema: Record; + }; + metadata?: Record | null; +}; import { waitUntil } from "@vercel/functions"; import { @@ -714,7 +734,18 @@ const loginSchema = z.strictObject({ debugLogLevelDisabled: z.boolean().optional(), }); -export type SerializedBraintrustState = z.infer; +export type SerializedBraintrustState = { + appUrl: string; + appPublicUrl: string; + orgName: string; + apiUrl: string; + proxyUrl: string; + loginToken: string; + orgId?: string | null; + gitMetadataSettings?: GitMetadataSettings | null; + debugLogLevel?: "error" | "warn" | "info" | "debug"; + debugLogLevelDisabled?: boolean; +}; let stateNonce = 0; @@ -1863,7 +1894,10 @@ const attachmentMetadataSchema = z.object({ status: attachmentStatusSchema, }); -type AttachmentMetadata = z.infer; +type AttachmentMetadata = { + downloadUrl: string; + status: AttachmentStatus; +}; /** * A readonly alternative to `Attachment`, which can be used for fetching @@ -2784,14 +2818,22 @@ function castLogger( return logger as unknown as Logger; } -export const logs3OverflowUploadSchema = z.object({ - method: z.enum(["PUT", "POST"]), - signedUrl: z.string().url(), - headers: z.record(z.string()).optional(), - fields: z.record(z.string()).optional(), - key: z.string().min(1), -}); -export type Logs3OverflowUpload = z.infer; +export type Logs3OverflowUpload = { + method: "PUT" | "POST"; + signedUrl: string; + headers?: Record; + fields?: Record; + key: string; +}; + +export const logs3OverflowUploadSchema: z.ZodType = + z.object({ + method: z.enum(["PUT", "POST"]), + signedUrl: z.string().url(), + headers: z.record(z.string()).optional(), + fields: z.record(z.string()).optional(), + key: z.string().min(1), + }); export type Logs3OverflowInputRow = { object_ids: Record; @@ -8375,7 +8417,7 @@ export class Dataset< metadata?: Record; tags?: string[]; output?: unknown; - origin?: ObjectReferenceType; + origin?: ObjectReference; isMerge?: boolean; }): LazyValue { return new LazyValue(async () => { @@ -8434,7 +8476,7 @@ export class Dataset< readonly metadata?: Record; readonly id?: string; readonly output?: unknown; - readonly origin?: ObjectReferenceType; + readonly origin?: ObjectReference; }): string { this.validateEvent({ metadata, expected, output, tags }); diff --git a/js/src/prompt-schemas.ts b/js/src/prompt-schemas.ts index c4203f90e..46ad5f307 100644 --- a/js/src/prompt-schemas.ts +++ b/js/src/prompt-schemas.ts @@ -1,15 +1,23 @@ import { z } from "zod/v3"; import { ToolFunctionDefinition as toolFunctionDefinitionSchema, - type ToolFunctionDefinitionType as ToolFunctionDefinition, ChatCompletionMessageParam as chatCompletionMessageParamSchema, ModelParams as modelParamsSchema, - type PromptBlockDataType as PromptBlockData, - type PromptDataType as PromptData, } from "./generated_types"; +import type { + ToolFunctionDefinitionType as ToolFunctionDefinition, + ChatCompletionMessageParamType, + ModelParamsType, + PromptBlockDataType as PromptBlockData, + PromptDataType as PromptData, +} from "./generated_plain_types"; // This roughly maps to promptBlockDataSchema, but is more ergonomic for the user. -export const promptContentsSchema = z.union([ +export type PromptContents = + | { prompt: string } + | { messages: ChatCompletionMessageParamType[] }; + +const internalPromptContentsSchema = z.union([ z.object({ prompt: z.string(), }), @@ -17,10 +25,20 @@ export const promptContentsSchema = z.union([ messages: z.array(chatCompletionMessageParamSchema), }), ]); +export const promptContentsSchema: z.ZodType< + PromptContents, + z.ZodTypeDef, + unknown +> = internalPromptContentsSchema; -export type PromptContents = z.infer; +export type PromptDefinition = PromptContents & { + model: string; + params?: ModelParamsType; + templateFormat?: "mustache" | "nunjucks" | "none"; + environments?: string[]; +}; -export const promptDefinitionSchema = promptContentsSchema.and( +const internalPromptDefinitionSchema = internalPromptContentsSchema.and( z.object({ model: z.string(), params: modelParamsSchema.optional(), @@ -28,18 +46,27 @@ export const promptDefinitionSchema = promptContentsSchema.and( environments: z.array(z.string()).optional(), }), ); +export const promptDefinitionSchema: z.ZodType< + PromptDefinition, + z.ZodTypeDef, + unknown +> = internalPromptDefinitionSchema; -export type PromptDefinition = z.infer; - -export const promptDefinitionWithToolsSchema = promptDefinitionSchema.and( - z.object({ - tools: z.array(toolFunctionDefinitionSchema).optional(), - }), -); +export type PromptDefinitionWithTools = PromptDefinition & { + tools?: ToolFunctionDefinition[]; +}; -export type PromptDefinitionWithTools = z.infer< - typeof promptDefinitionWithToolsSchema ->; +const internalPromptDefinitionWithToolsSchema = + internalPromptDefinitionSchema.and( + z.object({ + tools: z.array(toolFunctionDefinitionSchema).optional(), + }), + ); +export const promptDefinitionWithToolsSchema: z.ZodType< + PromptDefinitionWithTools, + z.ZodTypeDef, + unknown +> = internalPromptDefinitionWithToolsSchema; export function promptDefinitionToPromptData( promptDefinition: PromptDefinition, diff --git a/js/src/public-types.test.ts b/js/src/public-types.test.ts new file mode 100644 index 000000000..0583994df --- /dev/null +++ b/js/src/public-types.test.ts @@ -0,0 +1,69 @@ +import { expectTypeOf, test } from "vitest"; +import { z } from "zod/v3"; + +import { + AttachmentReference, + braintrustStreamChunkSchema, + logs3OverflowUploadSchema, + promptContentsSchema, + promptDefinitionSchema, + promptDefinitionWithToolsSchema, + type BraintrustStreamChunk, + type EvalParameters, + type Logs3OverflowUpload, + type PromptContents, + type PromptDefinition, + type PromptDefinitionWithTools, +} from "./exports"; +import type { InferParameters } from "./eval-parameters"; +import type { AttachmentReferenceType } from "./generated_plain_types"; +import type { Prompt } from "./logger"; +import { + spanComponentsV3Schema, + type SpanComponentsV3Data, +} from "../util/span_identifier_v3"; +import { + spanComponentsV4Schema, + type SpanComponentsV4Data, +} from "../util/span_identifier_v4"; + +test("exported validators preserve their public output types", () => { + expectTypeOf< + z.infer + >().toEqualTypeOf(); + expectTypeOf< + z.infer + >().toEqualTypeOf(); + expectTypeOf< + z.infer + >().toEqualTypeOf(); + expectTypeOf< + z.infer + >().toEqualTypeOf(); + expectTypeOf< + z.infer + >().toEqualTypeOf(); + expectTypeOf< + z.infer + >().toEqualTypeOf(); + expectTypeOf< + z.infer + >().toEqualTypeOf(); + expectTypeOf< + z.infer + >().toEqualTypeOf(); +}); + +test("evaluation parameters retain custom schema inference", () => { + const parameters = { + subject: z.string(), + model: { type: "model" as const }, + prompt: { type: "prompt" as const }, + } satisfies EvalParameters; + + expectTypeOf>().toEqualTypeOf<{ + subject: string; + model: string; + prompt: Prompt; + }>(); +}); diff --git a/js/src/sandbox.ts b/js/src/sandbox.ts index 8517c801d..cfafa8c98 100644 --- a/js/src/sandbox.ts +++ b/js/src/sandbox.ts @@ -1,6 +1,6 @@ import { z } from "zod/v3"; import { slugify } from "../util/string_util"; -import { type IfExistsType } from "./generated_types"; +import type { IfExistsType } from "./generated_plain_types"; import { type BraintrustState, _internalGetGlobalState } from "./logger"; /** diff --git a/js/tests/api-compatibility/api-compatibility.test.ts b/js/tests/api-compatibility/api-compatibility.test.ts index f11e3d847..2fd785357 100644 --- a/js/tests/api-compatibility/api-compatibility.test.ts +++ b/js/tests/api-compatibility/api-compatibility.test.ts @@ -2948,6 +2948,56 @@ describe("API Compatibility", () => { expect(fs.existsSync(path.join(tempDir, "package"))).toBe(true); }); + test("keeps public declarations free of expanded Zod schema graphs", () => { + const declarationRoot = path.join(__dirname, "..", ".."); + const publicDeclarationPaths = [ + "dist/index.d.ts", + "dist/browser.d.ts", + "util/dist/index.d.ts", + ]; + + for (const declarationTypesPath of publicDeclarationPaths) { + const declarationPath = path.join(declarationRoot, declarationTypesPath); + const declaration = fs.readFileSync(declarationPath, "utf8"); + + expect(declaration).not.toMatch(/z\.infer { if (!publishedVersion) { console.log("Skipping test: No published version available"); diff --git a/js/util/git_fields.ts b/js/util/git_fields.ts index 8eb06d625..482a43526 100644 --- a/js/util/git_fields.ts +++ b/js/util/git_fields.ts @@ -1,4 +1,4 @@ -import { GitMetadataSettingsType as GitMetadataSettings } from "./generated_types"; +import type { GitMetadataSettingsType as GitMetadataSettings } from "./generated_plain_types"; export function mergeGitMetadataSettings( s1: GitMetadataSettings, diff --git a/js/util/object.ts b/js/util/object.ts index 7ea593005..941c6264a 100644 --- a/js/util/object.ts +++ b/js/util/object.ts @@ -1,7 +1,7 @@ -import { +import type { AsyncScoringControlType as AsyncScoringControl, - type ObjectReferenceType, -} from "./generated_types"; + ObjectReferenceType, +} from "./generated_plain_types"; import { Source, ASYNC_SCORING_CONTROL_FIELD, diff --git a/js/util/span_identifier_v3.ts b/js/util/span_identifier_v3.ts index f057f798b..78a753d53 100644 --- a/js/util/span_identifier_v3.ts +++ b/js/util/span_identifier_v3.ts @@ -38,7 +38,8 @@ export enum SpanObjectTypeV3 { PLAYGROUND_LOGS = 3, } -export const spanObjectTypeV3EnumSchema = z.nativeEnum(SpanObjectTypeV3); +export const spanObjectTypeV3EnumSchema: z.ZodType = + z.nativeEnum(SpanObjectTypeV3); export function spanObjectTypeV3ToTypedString( objectType: SpanObjectTypeV3, @@ -81,7 +82,35 @@ const _INTERNAL_SPAN_COMPONENT_UUID_FIELDS_ID_TO_NAME: Record< [InternalSpanComponentUUIDFields.ROOT_SPAN_ID]: "root_span_id", }; -export const spanComponentsV3Schema = z +type SpanObjectMetadata = + | { + object_id?: string | null; + compute_object_metadata_args?: null; + } + | { + object_id?: null; + compute_object_metadata_args: Record; + }; + +type SpanRowIds = + | { + row_id: string; + span_id: string; + root_span_id: string; + } + | { + row_id?: null; + span_id?: null; + root_span_id?: null; + }; + +export type SpanComponentsV3Data = { + object_type: SpanObjectTypeV3; + propagated_event?: Record | null; +} & SpanObjectMetadata & + SpanRowIds; + +export const spanComponentsV3Schema: z.ZodType = z .object({ object_type: spanObjectTypeV3EnumSchema, // TODO(manu): We should have a more elaborate zod schema for @@ -118,8 +147,6 @@ export const spanComponentsV3Schema = z ]), ); -export type SpanComponentsV3Data = z.infer; - export class SpanComponentsV3 { constructor(public data: SpanComponentsV3Data) {} diff --git a/js/util/span_identifier_v4.ts b/js/util/span_identifier_v4.ts index e92e3eddf..771d72aec 100644 --- a/js/util/span_identifier_v4.ts +++ b/js/util/span_identifier_v4.ts @@ -5,6 +5,7 @@ import { SpanComponentsV3, SpanObjectTypeV3, spanObjectTypeV3EnumSchema, + type SpanComponentsV3Data, } from "./span_identifier_v3"; import { ParentExperimentIds, @@ -19,7 +20,7 @@ import { uint8ArrayToString, } from "./bytes"; import { z } from "zod/v3"; -import { InvokeFunctionType as InvokeFunctionRequest } from "./generated_types"; +import type { InvokeFunctionType as InvokeFunctionRequest } from "./generated_plain_types"; import { mergeDicts } from "./object_util"; const ENCODING_VERSION_NUMBER_V4 = 4; @@ -88,7 +89,9 @@ const FIELDS_ID_TO_NAME: Record = { [Fields.ROOT_SPAN_ID]: "root_span_id", }; -export const spanComponentsV4Schema = z +export type SpanComponentsV4Data = SpanComponentsV3Data; + +export const spanComponentsV4Schema: z.ZodType = z .object({ object_type: spanObjectTypeV3EnumSchema, propagated_event: z.record(z.unknown()).nullish(), @@ -122,8 +125,6 @@ export const spanComponentsV4Schema = z ]), ); -export type SpanComponentsV4Data = z.infer; - export class SpanComponentsV4 { constructor(public data: SpanComponentsV4Data) {} From 55dd6c95a73195b5c44b5cc8381b5c7ce9d8f1f5 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 26 Aug 2026 20:09:43 +0200 Subject: [PATCH 06/19] ref!: Remove plugins as part of public API (#2386) ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) Removes Plugins and unsubscribe functionality. Reasoning: Plugins are a bit of an unnecessary API to be exposed because they just attach stuff and un-attach stuff. The user can very easily just manage this by themselves. Also, unsubscribe is literally called from nowhere in the SDK. --- .changeset/remove-plugins-public-api.md | 5 + .../auto-instrumentations/configs/ai-sdk.ts | 4 +- .../configs/anthropic.ts | 2 +- .../configs/bedrock-runtime.ts | 2 +- .../configs/claude-agent-sdk.ts | 2 +- .../configs/cloudflare-agents.ts | 2 +- .../configs/cloudflare-ai-chat.ts | 2 +- .../configs/cloudflare-think.ts | 2 +- .../auto-instrumentations/configs/cohere.ts | 2 +- .../configs/cursor-sdk.ts | 2 +- .../auto-instrumentations/configs/genkit.ts | 2 +- .../configs/github-copilot.ts | 2 +- .../configs/google-adk.ts | 2 +- .../configs/google-genai.ts | 2 +- js/src/auto-instrumentations/configs/groq.ts | 2 +- .../configs/huggingface-transformers.ts | 2 +- .../configs/huggingface.ts | 2 +- .../configs/langchain.ts | 2 +- .../configs/langsmith.ts | 2 +- .../auto-instrumentations/configs/mistral.ts | 2 +- .../auto-instrumentations/configs/ollama.ts | 2 +- .../configs/openai-agents.ts | 2 +- .../configs/openai-codex.ts | 2 +- .../auto-instrumentations/configs/openai.ts | 2 +- .../configs/openrouter-agent.ts | 2 +- .../configs/openrouter.ts | 2 +- .../configs/pi-coding-agent.ts | 2 +- .../configs/strands-agent-sdk.ts | 2 +- .../auto-instrumentations/configs/voyageai.ts | 2 +- .../auto-instrumentations/loader/cjs-patch.ts | 2 +- .../loader/special-case-patches.ts | 4 +- js/src/exports.ts | 2 +- js/src/imports.test.ts | 6 +- js/src/instrumentation/README.md | 71 +- .../auto-instrumentation-suppression.ts | 8 +- .../instrumentation/braintrust-plugin.test.ts | 1791 ----------------- js/src/instrumentation/braintrust-plugin.ts | 402 ---- .../core/channel-tracing.test.ts | 225 +-- .../instrumentation/core/channel-tracing.ts | 45 +- js/src/instrumentation/core/channel.ts | 55 - js/src/instrumentation/core/index.ts | 26 +- js/src/instrumentation/core/plugin.ts | 505 ----- js/src/instrumentation/core/types.ts | 89 +- js/src/instrumentation/index.test.ts | 13 + js/src/instrumentation/index.ts | 34 +- .../instrumentation-consumers.test.ts | 215 ++ .../instrumentation-consumers.ts | 159 ++ .../plugins/cloudflare-agents-plugin.ts | 140 -- .../instrumentation/plugins/openai-plugin.ts | 613 ------ .../{plugins => providers}/ai-sdk-channels.ts | 0 .../ai-sdk-instrumentation.streaming.test.ts} | 229 +-- .../ai-sdk-instrumentation.test.ts} | 80 +- .../ai-sdk-instrumentation.ts} | 859 ++++---- .../ai-sdk-v7-telemetry.test.ts | 0 .../anthropic-channels.ts | 0 .../anthropic-instrumentation.test.ts} | 2 +- .../anthropic-instrumentation.ts} | 45 +- ...nthropic-sessions-instrumentation.test.ts} | 11 +- .../bedrock-runtime-channels.ts | 0 .../bedrock-runtime-common.ts | 0 .../bedrock-runtime-instrumentation.test.ts} | 6 +- .../bedrock-runtime-instrumentation.ts} | 29 +- .../claude-agent-sdk-channels.ts | 0 ...ude-agent-sdk-instrumentation-constants.ts | 0 ...ent-sdk-instrumentation.streaming.test.ts} | 2 +- .../claude-agent-sdk-instrumentation.test.ts} | 122 +- .../claude-agent-sdk-instrumentation.ts} | 25 +- .../claude-agent-sdk-local-tool-context.ts | 0 .../claude-agent-sdk-local-tool-spans.ts | 0 .../cloudflare-agents-channels.ts | 0 ...cloudflare-agents-instrumentation.test.ts} | 26 +- .../cloudflare-agents-instrumentation.ts | 129 ++ .../cloudflare-ai-chat-channels.ts | 0 .../cloudflare-ai-chat-consumer.test.ts} | 28 +- .../cloudflare-ai-chat-consumer.ts} | 40 +- .../cloudflare-ai-chat-instrumentation.ts | 0 .../cloudflare-think-channels.ts | 0 .../cloudflare-think-context.ts | 0 .../cloudflare-think-instrumentation.test.ts} | 2 +- .../cloudflare-think-instrumentation.ts} | 35 +- .../{plugins => providers}/cohere-channels.ts | 0 .../cohere-instrumentation.test.ts} | 2 +- .../cohere-instrumentation.ts} | 129 +- .../cursor-sdk-channels.ts | 0 .../cursor-sdk-instrumentation.test.ts} | 18 +- .../cursor-sdk-instrumentation.ts} | 32 +- .../eve-instrumentation.test.ts} | 12 +- .../eve-instrumentation.ts} | 0 .../flue-instrumentation.test.ts} | 2 +- .../flue-instrumentation.ts} | 0 .../{plugins => providers}/genkit-channels.ts | 0 .../genkit-instrumentation.test.ts} | 13 +- .../genkit-instrumentation.ts} | 143 +- .../github-copilot-channels.ts | 0 .../github-copilot-instrumentation.test.ts} | 40 +- .../github-copilot-instrumentation.ts} | 29 +- .../google-adk-channels.ts | 0 .../google-adk-instrumentation.test.ts} | 78 +- .../google-adk-instrumentation.ts} | 59 +- .../google-genai-channels.ts | 0 .../google-genai-instrumentation.test.ts} | 63 +- .../google-genai-instrumentation.ts} | 176 +- .../{plugins => providers}/groq-channels.ts | 0 .../groq-instrumentation.test.ts} | 2 +- .../groq-instrumentation.ts} | 92 +- .../harness-agent-context.test.ts | 0 .../huggingface-channels.ts | 0 .../huggingface-instrumentation.test.ts} | 2 +- .../huggingface-instrumentation.ts} | 102 +- .../huggingface-transformers-channels.ts | 0 ...face-transformers-instrumentation.test.ts} | 4 +- ...ggingface-transformers-instrumentation.ts} | 78 +- .../instrumentation-names.test.ts | 0 .../langchain-channels.ts | 0 .../langchain-instrumentation.test.ts} | 31 +- .../langchain-instrumentation.ts} | 25 +- .../langsmith-channels.ts | 0 .../langsmith-instrumentation.test.ts} | 41 +- .../langsmith-instrumentation.ts} | 36 +- .../mistral-channels.ts | 0 .../mistral-instrumentation.test.ts} | 2 +- .../mistral-instrumentation.ts} | 275 ++- .../{plugins => providers}/ollama-channels.ts | 0 .../ollama-instrumentation.test.ts} | 2 +- .../ollama-instrumentation.ts} | 72 +- .../openai-agents-channels.ts | 0 .../openai-agents-instrumentation.test.ts} | 13 +- .../openai-agents-instrumentation.ts} | 31 +- .../openai-agents-trace-processor.ts | 0 .../{plugins => providers}/openai-channels.ts | 0 .../openai-codex-channels.ts | 0 .../openai-codex-instrumentation.test.ts} | 8 +- .../openai-codex-instrumentation.ts} | 29 +- .../openai-instrumentation.test.ts} | 2 +- .../providers/openai-instrumentation.ts | 573 ++++++ .../openrouter-agent-channels.ts | 0 .../openrouter-agent-instrumentation.test.ts} | 6 +- .../openrouter-agent-instrumentation.ts} | 178 +- .../openrouter-channels.ts | 0 .../openrouter-instrumentation.test.ts} | 6 +- .../openrouter-instrumentation.ts} | 437 ++-- .../pi-coding-agent-channels.ts | 0 .../pi-coding-agent-instrumentation.test.ts} | 54 +- .../pi-coding-agent-instrumentation.ts} | 114 +- .../strands-agent-sdk-channels.ts | 0 ...strands-agent-sdk-instrumentation.test.ts} | 30 +- .../strands-agent-sdk-instrumentation.ts} | 37 +- .../voyageai-channels.ts | 0 .../voyageai-instrumentation.test.ts} | 4 +- .../voyageai-instrumentation.ts} | 77 +- js/src/instrumentation/registry.test.ts | 46 +- js/src/instrumentation/registry.ts | 46 +- js/src/wrappers/ai-sdk/ai-sdk.ts | 2 +- .../wrappers/ai-sdk/harness-agent-context.ts | 7 +- js/src/wrappers/ai-sdk/telemetry.ts | 2 +- js/src/wrappers/anthropic.ts | 2 +- js/src/wrappers/bedrock-runtime.ts | 4 +- .../claude-agent-sdk/claude-agent-sdk.ts | 6 +- js/src/wrappers/cloudflare-agent.ts | 2 +- js/src/wrappers/cloudflare-ai-chat.ts | 2 +- js/src/wrappers/cloudflare-think.ts | 2 +- js/src/wrappers/cohere.ts | 2 +- js/src/wrappers/cursor-sdk.ts | 2 +- js/src/wrappers/genkit.test.ts | 2 +- js/src/wrappers/genkit.ts | 2 +- js/src/wrappers/github-copilot.ts | 2 +- js/src/wrappers/google-adk.ts | 2 +- js/src/wrappers/google-genai.ts | 2 +- js/src/wrappers/groq.ts | 2 +- js/src/wrappers/huggingface-transformers.ts | 2 +- js/src/wrappers/huggingface.ts | 2 +- js/src/wrappers/langsmith.ts | 2 +- js/src/wrappers/mistral.ts | 2 +- js/src/wrappers/oai.ts | 2 +- js/src/wrappers/oai_responses.ts | 2 +- js/src/wrappers/ollama.test.ts | 2 +- js/src/wrappers/ollama.ts | 2 +- js/src/wrappers/openai-codex.ts | 2 +- js/src/wrappers/openai-promise-utils.ts | 2 +- js/src/wrappers/openrouter-agent.test.ts | 2 +- js/src/wrappers/openrouter-agent.ts | 2 +- js/src/wrappers/openrouter.ts | 2 +- js/src/wrappers/pi-coding-agent.ts | 2 +- js/src/wrappers/strands-agent-sdk.ts | 2 +- js/src/wrappers/voyageai.ts | 2 +- 185 files changed, 3059 insertions(+), 6407 deletions(-) create mode 100644 .changeset/remove-plugins-public-api.md delete mode 100644 js/src/instrumentation/braintrust-plugin.test.ts delete mode 100644 js/src/instrumentation/braintrust-plugin.ts delete mode 100644 js/src/instrumentation/core/channel.ts delete mode 100644 js/src/instrumentation/core/plugin.ts create mode 100644 js/src/instrumentation/index.test.ts create mode 100644 js/src/instrumentation/instrumentation-consumers.test.ts create mode 100644 js/src/instrumentation/instrumentation-consumers.ts delete mode 100644 js/src/instrumentation/plugins/cloudflare-agents-plugin.ts delete mode 100644 js/src/instrumentation/plugins/openai-plugin.ts rename js/src/instrumentation/{plugins => providers}/ai-sdk-channels.ts (100%) rename js/src/instrumentation/{plugins/ai-sdk-plugin.streaming.test.ts => providers/ai-sdk-instrumentation.streaming.test.ts} (92%) rename js/src/instrumentation/{plugins/ai-sdk-plugin.test.ts => providers/ai-sdk-instrumentation.test.ts} (96%) rename js/src/instrumentation/{plugins/ai-sdk-plugin.ts => providers/ai-sdk-instrumentation.ts} (85%) rename js/src/instrumentation/{plugins => providers}/ai-sdk-v7-telemetry.test.ts (100%) rename js/src/instrumentation/{plugins => providers}/anthropic-channels.ts (100%) rename js/src/instrumentation/{plugins/anthropic-plugin.test.ts => providers/anthropic-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/anthropic-plugin.ts => providers/anthropic-instrumentation.ts} (98%) rename js/src/instrumentation/{plugins/anthropic-sessions-plugin.test.ts => providers/anthropic-sessions-instrumentation.test.ts} (97%) rename js/src/instrumentation/{plugins => providers}/bedrock-runtime-channels.ts (100%) rename js/src/instrumentation/{plugins => providers}/bedrock-runtime-common.ts (100%) rename js/src/instrumentation/{plugins/bedrock-runtime-plugin.test.ts => providers/bedrock-runtime-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/bedrock-runtime-plugin.ts => providers/bedrock-runtime-instrumentation.ts} (97%) rename js/src/instrumentation/{plugins => providers}/claude-agent-sdk-channels.ts (100%) rename js/src/instrumentation/{plugins => providers}/claude-agent-sdk-instrumentation-constants.ts (100%) rename js/src/instrumentation/{plugins/claude-agent-sdk-plugin.streaming.test.ts => providers/claude-agent-sdk-instrumentation.streaming.test.ts} (99%) rename js/src/instrumentation/{plugins/claude-agent-sdk-plugin.test.ts => providers/claude-agent-sdk-instrumentation.test.ts} (86%) rename js/src/instrumentation/{plugins/claude-agent-sdk-plugin.ts => providers/claude-agent-sdk-instrumentation.ts} (99%) rename js/src/instrumentation/{plugins => providers}/claude-agent-sdk-local-tool-context.ts (100%) rename js/src/instrumentation/{plugins => providers}/claude-agent-sdk-local-tool-spans.ts (100%) rename js/src/instrumentation/{plugins => providers}/cloudflare-agents-channels.ts (100%) rename js/src/instrumentation/{plugins/cloudflare-agents-plugin.test.ts => providers/cloudflare-agents-instrumentation.test.ts} (88%) create mode 100644 js/src/instrumentation/providers/cloudflare-agents-instrumentation.ts rename js/src/instrumentation/{plugins => providers}/cloudflare-ai-chat-channels.ts (100%) rename js/src/instrumentation/{plugins/cloudflare-ai-chat-plugin.test.ts => providers/cloudflare-ai-chat-consumer.test.ts} (94%) rename js/src/instrumentation/{plugins/cloudflare-ai-chat-plugin.ts => providers/cloudflare-ai-chat-consumer.ts} (94%) rename js/src/instrumentation/{plugins => providers}/cloudflare-ai-chat-instrumentation.ts (100%) rename js/src/instrumentation/{plugins => providers}/cloudflare-think-channels.ts (100%) rename js/src/instrumentation/{plugins => providers}/cloudflare-think-context.ts (100%) rename js/src/instrumentation/{plugins/cloudflare-think-plugin.test.ts => providers/cloudflare-think-instrumentation.test.ts} (98%) rename js/src/instrumentation/{plugins/cloudflare-think-plugin.ts => providers/cloudflare-think-instrumentation.ts} (93%) rename js/src/instrumentation/{plugins => providers}/cohere-channels.ts (100%) rename js/src/instrumentation/{plugins/cohere-plugin.test.ts => providers/cohere-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/cohere-plugin.ts => providers/cohere-instrumentation.ts} (89%) rename js/src/instrumentation/{plugins => providers}/cursor-sdk-channels.ts (100%) rename js/src/instrumentation/{plugins/cursor-sdk-plugin.test.ts => providers/cursor-sdk-instrumentation.test.ts} (94%) rename js/src/instrumentation/{plugins/cursor-sdk-plugin.ts => providers/cursor-sdk-instrumentation.ts} (98%) rename js/src/instrumentation/{plugins/eve-plugin.test.ts => providers/eve-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/eve-plugin.ts => providers/eve-instrumentation.ts} (100%) rename js/src/instrumentation/{plugins/flue-plugin.test.ts => providers/flue-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/flue-plugin.ts => providers/flue-instrumentation.ts} (100%) rename js/src/instrumentation/{plugins => providers}/genkit-channels.ts (100%) rename js/src/instrumentation/{plugins/genkit-plugin.test.ts => providers/genkit-instrumentation.test.ts} (91%) rename js/src/instrumentation/{plugins/genkit-plugin.ts => providers/genkit-instrumentation.ts} (85%) rename js/src/instrumentation/{plugins => providers}/github-copilot-channels.ts (100%) rename js/src/instrumentation/{plugins/github-copilot-plugin.test.ts => providers/github-copilot-instrumentation.test.ts} (79%) rename js/src/instrumentation/{plugins/github-copilot-plugin.ts => providers/github-copilot-instrumentation.ts} (97%) rename js/src/instrumentation/{plugins => providers}/google-adk-channels.ts (100%) rename js/src/instrumentation/{plugins/google-adk-plugin.test.ts => providers/google-adk-instrumentation.test.ts} (91%) rename js/src/instrumentation/{plugins/google-adk-plugin.ts => providers/google-adk-instrumentation.ts} (95%) rename js/src/instrumentation/{plugins => providers}/google-genai-channels.ts (100%) rename js/src/instrumentation/{plugins/google-genai-plugin.test.ts => providers/google-genai-instrumentation.test.ts} (95%) rename js/src/instrumentation/{plugins/google-genai-plugin.ts => providers/google-genai-instrumentation.ts} (92%) rename js/src/instrumentation/{plugins => providers}/groq-channels.ts (100%) rename js/src/instrumentation/{plugins/groq-plugin.test.ts => providers/groq-instrumentation.test.ts} (98%) rename js/src/instrumentation/{plugins/groq-plugin.ts => providers/groq-instrumentation.ts} (58%) rename js/src/instrumentation/{plugins => providers}/harness-agent-context.test.ts (100%) rename js/src/instrumentation/{plugins => providers}/huggingface-channels.ts (100%) rename js/src/instrumentation/{plugins/huggingface-plugin.test.ts => providers/huggingface-instrumentation.test.ts} (98%) rename js/src/instrumentation/{plugins/huggingface-plugin.ts => providers/huggingface-instrumentation.ts} (87%) rename js/src/instrumentation/{plugins => providers}/huggingface-transformers-channels.ts (100%) rename js/src/instrumentation/{plugins/huggingface-transformers-plugin.test.ts => providers/huggingface-transformers-instrumentation.test.ts} (97%) rename js/src/instrumentation/{plugins/huggingface-transformers-plugin.ts => providers/huggingface-transformers-instrumentation.ts} (82%) rename js/src/instrumentation/{plugins => providers}/instrumentation-names.test.ts (100%) rename js/src/instrumentation/{plugins => providers}/langchain-channels.ts (100%) rename js/src/instrumentation/{plugins/langchain-plugin.test.ts => providers/langchain-instrumentation.test.ts} (79%) rename js/src/instrumentation/{plugins/langchain-plugin.ts => providers/langchain-instrumentation.ts} (89%) rename js/src/instrumentation/{plugins => providers}/langsmith-channels.ts (100%) rename js/src/instrumentation/{plugins/langsmith-plugin.test.ts => providers/langsmith-instrumentation.test.ts} (88%) rename js/src/instrumentation/{plugins/langsmith-plugin.ts => providers/langsmith-instrumentation.ts} (95%) rename js/src/instrumentation/{plugins => providers}/mistral-channels.ts (100%) rename js/src/instrumentation/{plugins/mistral-plugin.test.ts => providers/mistral-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/mistral-plugin.ts => providers/mistral-instrumentation.ts} (77%) rename js/src/instrumentation/{plugins => providers}/ollama-channels.ts (100%) rename js/src/instrumentation/{plugins/ollama-plugin.test.ts => providers/ollama-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/ollama-plugin.ts => providers/ollama-instrumentation.ts} (91%) rename js/src/instrumentation/{plugins => providers}/openai-agents-channels.ts (100%) rename js/src/instrumentation/{plugins/openai-agents-plugin.test.ts => providers/openai-agents-instrumentation.test.ts} (88%) rename js/src/instrumentation/{plugins/openai-agents-plugin.ts => providers/openai-agents-instrumentation.ts} (80%) rename js/src/instrumentation/{plugins => providers}/openai-agents-trace-processor.ts (100%) rename js/src/instrumentation/{plugins => providers}/openai-channels.ts (100%) rename js/src/instrumentation/{plugins => providers}/openai-codex-channels.ts (100%) rename js/src/instrumentation/{plugins/openai-codex-plugin.test.ts => providers/openai-codex-instrumentation.test.ts} (92%) rename js/src/instrumentation/{plugins/openai-codex-plugin.ts => providers/openai-codex-instrumentation.ts} (98%) rename js/src/instrumentation/{plugins/openai-plugin.test.ts => providers/openai-instrumentation.test.ts} (99%) create mode 100644 js/src/instrumentation/providers/openai-instrumentation.ts rename js/src/instrumentation/{plugins => providers}/openrouter-agent-channels.ts (100%) rename js/src/instrumentation/{plugins/openrouter-agent-plugin.test.ts => providers/openrouter-agent-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/openrouter-agent-plugin.ts => providers/openrouter-agent-instrumentation.ts} (91%) rename js/src/instrumentation/{plugins => providers}/openrouter-channels.ts (100%) rename js/src/instrumentation/{plugins/openrouter-plugin.test.ts => providers/openrouter-instrumentation.test.ts} (99%) rename js/src/instrumentation/{plugins/openrouter-plugin.ts => providers/openrouter-instrumentation.ts} (80%) rename js/src/instrumentation/{plugins => providers}/pi-coding-agent-channels.ts (100%) rename js/src/instrumentation/{plugins/pi-coding-agent-plugin.test.ts => providers/pi-coding-agent-instrumentation.test.ts} (91%) rename js/src/instrumentation/{plugins/pi-coding-agent-plugin.ts => providers/pi-coding-agent-instrumentation.ts} (93%) rename js/src/instrumentation/{plugins => providers}/strands-agent-sdk-channels.ts (100%) rename js/src/instrumentation/{plugins/strands-agent-sdk-plugin.test.ts => providers/strands-agent-sdk-instrumentation.test.ts} (95%) rename js/src/instrumentation/{plugins/strands-agent-sdk-plugin.ts => providers/strands-agent-sdk-instrumentation.ts} (97%) rename js/src/instrumentation/{plugins => providers}/voyageai-channels.ts (100%) rename js/src/instrumentation/{plugins/voyageai-plugin.test.ts => providers/voyageai-instrumentation.test.ts} (98%) rename js/src/instrumentation/{plugins/voyageai-plugin.ts => providers/voyageai-instrumentation.ts} (89%) diff --git a/.changeset/remove-plugins-public-api.md b/.changeset/remove-plugins-public-api.md new file mode 100644 index 000000000..21275cc2e --- /dev/null +++ b/.changeset/remove-plugins-public-api.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref!: Remove plugins as part of public API diff --git a/js/src/auto-instrumentations/configs/ai-sdk.ts b/js/src/auto-instrumentations/configs/ai-sdk.ts index cdbb0c1f3..d38234e5e 100644 --- a/js/src/auto-instrumentations/configs/ai-sdk.ts +++ b/js/src/auto-instrumentations/configs/ai-sdk.ts @@ -2,7 +2,7 @@ import type { InstrumentationConfig } from "../orchestrion-js"; import { aiSDKChannels, harnessAgentChannels, -} from "../../instrumentation/plugins/ai-sdk-channels"; +} from "../../instrumentation/providers/ai-sdk-channels"; /** * Instrumentation configurations for the Vercel AI SDK. @@ -222,7 +222,7 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ }, // AI SDK v7 exposes its telemetry lifecycle through a dispatcher created for - // each operation. We patch that dispatcher in the plugin instead of rewriting + // each operation. We patch that dispatcher in the consumer instead of rewriting // the module to call registerTelemetry(). { channelName: aiSDKChannels.v7CreateTelemetryDispatcher.channelName, diff --git a/js/src/auto-instrumentations/configs/anthropic.ts b/js/src/auto-instrumentations/configs/anthropic.ts index 3acaa4d75..862e3528b 100644 --- a/js/src/auto-instrumentations/configs/anthropic.ts +++ b/js/src/auto-instrumentations/configs/anthropic.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { anthropicChannels } from "../../instrumentation/plugins/anthropic-channels"; +import { anthropicChannels } from "../../instrumentation/providers/anthropic-channels"; /** * Instrumentation configurations for the Anthropic SDK. diff --git a/js/src/auto-instrumentations/configs/bedrock-runtime.ts b/js/src/auto-instrumentations/configs/bedrock-runtime.ts index 29b4d0f0d..91dddcfae 100644 --- a/js/src/auto-instrumentations/configs/bedrock-runtime.ts +++ b/js/src/auto-instrumentations/configs/bedrock-runtime.ts @@ -2,7 +2,7 @@ import type { InstrumentationConfig } from "@apm-js-collab/code-transformer"; import { smithyClientChannels, smithyCoreChannels, -} from "../../instrumentation/plugins/bedrock-runtime-channels"; +} from "../../instrumentation/providers/bedrock-runtime-channels"; export const bedrockRuntimeConfigs: InstrumentationConfig[] = [ { diff --git a/js/src/auto-instrumentations/configs/claude-agent-sdk.ts b/js/src/auto-instrumentations/configs/claude-agent-sdk.ts index 1cf2c6537..6b663df61 100644 --- a/js/src/auto-instrumentations/configs/claude-agent-sdk.ts +++ b/js/src/auto-instrumentations/configs/claude-agent-sdk.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { claudeAgentSDKChannels } from "../../instrumentation/plugins/claude-agent-sdk-channels"; +import { claudeAgentSDKChannels } from "../../instrumentation/providers/claude-agent-sdk-channels"; /** * Instrumentation configuration for the Claude Agent SDK. diff --git a/js/src/auto-instrumentations/configs/cloudflare-agents.ts b/js/src/auto-instrumentations/configs/cloudflare-agents.ts index 0b655bb78..b18372cb1 100644 --- a/js/src/auto-instrumentations/configs/cloudflare-agents.ts +++ b/js/src/auto-instrumentations/configs/cloudflare-agents.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { cloudflareAgentsChannels } from "../../instrumentation/plugins/cloudflare-agents-channels"; +import { cloudflareAgentsChannels } from "../../instrumentation/providers/cloudflare-agents-channels"; const cloudflareAgentsVersionRange = ">=0.17.0 <0.18.0"; diff --git a/js/src/auto-instrumentations/configs/cloudflare-ai-chat.ts b/js/src/auto-instrumentations/configs/cloudflare-ai-chat.ts index 02f081612..1a603bffc 100644 --- a/js/src/auto-instrumentations/configs/cloudflare-ai-chat.ts +++ b/js/src/auto-instrumentations/configs/cloudflare-ai-chat.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { cloudflareAIChatChannels } from "../../instrumentation/plugins/cloudflare-ai-chat-channels"; +import { cloudflareAIChatChannels } from "../../instrumentation/providers/cloudflare-ai-chat-channels"; const cloudflareAIChatVersionRange = ">=0.9.0 <0.10.0"; diff --git a/js/src/auto-instrumentations/configs/cloudflare-think.ts b/js/src/auto-instrumentations/configs/cloudflare-think.ts index 61fb4e8ae..d0e270b0f 100644 --- a/js/src/auto-instrumentations/configs/cloudflare-think.ts +++ b/js/src/auto-instrumentations/configs/cloudflare-think.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { cloudflareThinkChannels } from "../../instrumentation/plugins/cloudflare-think-channels"; +import { cloudflareThinkChannels } from "../../instrumentation/providers/cloudflare-think-channels"; const cloudflareThinkVersionRange = ">=0.13.0 <0.14.0"; diff --git a/js/src/auto-instrumentations/configs/cohere.ts b/js/src/auto-instrumentations/configs/cohere.ts index 106980884..1ad936ac7 100644 --- a/js/src/auto-instrumentations/configs/cohere.ts +++ b/js/src/auto-instrumentations/configs/cohere.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { cohereChannels } from "../../instrumentation/plugins/cohere-channels"; +import { cohereChannels } from "../../instrumentation/providers/cohere-channels"; export const cohereConfigs: InstrumentationConfig[] = [ { diff --git a/js/src/auto-instrumentations/configs/cursor-sdk.ts b/js/src/auto-instrumentations/configs/cursor-sdk.ts index ecb845b4b..0b0eddb32 100644 --- a/js/src/auto-instrumentations/configs/cursor-sdk.ts +++ b/js/src/auto-instrumentations/configs/cursor-sdk.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { cursorSDKChannels } from "../../instrumentation/plugins/cursor-sdk-channels"; +import { cursorSDKChannels } from "../../instrumentation/providers/cursor-sdk-channels"; const cursorSDKVersionRange = ">=1.0.7 <2.0.0"; diff --git a/js/src/auto-instrumentations/configs/genkit.ts b/js/src/auto-instrumentations/configs/genkit.ts index 349635b01..5422a08fe 100644 --- a/js/src/auto-instrumentations/configs/genkit.ts +++ b/js/src/auto-instrumentations/configs/genkit.ts @@ -2,7 +2,7 @@ import type { InstrumentationConfig } from "../orchestrion-js"; import { genkitChannels, genkitCoreChannels, -} from "../../instrumentation/plugins/genkit-channels"; +} from "../../instrumentation/providers/genkit-channels"; const genkitVersionRange = ">=1.0.0 <2.0.0"; diff --git a/js/src/auto-instrumentations/configs/github-copilot.ts b/js/src/auto-instrumentations/configs/github-copilot.ts index 901f9e682..aed9eb7ed 100644 --- a/js/src/auto-instrumentations/configs/github-copilot.ts +++ b/js/src/auto-instrumentations/configs/github-copilot.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { gitHubCopilotChannels } from "../../instrumentation/plugins/github-copilot-channels"; +import { gitHubCopilotChannels } from "../../instrumentation/providers/github-copilot-channels"; export const gitHubCopilotConfigs: InstrumentationConfig[] = [ // ESM: CopilotClient.createSession diff --git a/js/src/auto-instrumentations/configs/google-adk.ts b/js/src/auto-instrumentations/configs/google-adk.ts index aeb72c44b..8c07fdbd7 100644 --- a/js/src/auto-instrumentations/configs/google-adk.ts +++ b/js/src/auto-instrumentations/configs/google-adk.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { googleADKChannels } from "../../instrumentation/plugins/google-adk-channels"; +import { googleADKChannels } from "../../instrumentation/providers/google-adk-channels"; const googleADKVersionRange = ">=0.1.0"; const googleADKBundledIndexV06VersionRange = ">=0.6.1 <0.7.0"; diff --git a/js/src/auto-instrumentations/configs/google-genai.ts b/js/src/auto-instrumentations/configs/google-genai.ts index 16edd1d5c..53fb24d43 100644 --- a/js/src/auto-instrumentations/configs/google-genai.ts +++ b/js/src/auto-instrumentations/configs/google-genai.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { googleGenAIChannels } from "../../instrumentation/plugins/google-genai-channels"; +import { googleGenAIChannels } from "../../instrumentation/providers/google-genai-channels"; /** * Instrumentation configurations for the Google GenAI SDK. diff --git a/js/src/auto-instrumentations/configs/groq.ts b/js/src/auto-instrumentations/configs/groq.ts index 864b2a166..5db99b980 100644 --- a/js/src/auto-instrumentations/configs/groq.ts +++ b/js/src/auto-instrumentations/configs/groq.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { groqChannels } from "../../instrumentation/plugins/groq-channels"; +import { groqChannels } from "../../instrumentation/providers/groq-channels"; export const groqConfigs: InstrumentationConfig[] = [ { diff --git a/js/src/auto-instrumentations/configs/huggingface-transformers.ts b/js/src/auto-instrumentations/configs/huggingface-transformers.ts index dd951ac25..781aab990 100644 --- a/js/src/auto-instrumentations/configs/huggingface-transformers.ts +++ b/js/src/auto-instrumentations/configs/huggingface-transformers.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { huggingFaceTransformersChannels } from "../../instrumentation/plugins/huggingface-transformers-channels"; +import { huggingFaceTransformersChannels } from "../../instrumentation/providers/huggingface-transformers-channels"; const moduleFiles = [ { diff --git a/js/src/auto-instrumentations/configs/huggingface.ts b/js/src/auto-instrumentations/configs/huggingface.ts index f4c9bec06..53b4d011f 100644 --- a/js/src/auto-instrumentations/configs/huggingface.ts +++ b/js/src/auto-instrumentations/configs/huggingface.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { huggingFaceChannels } from "../../instrumentation/plugins/huggingface-channels"; +import { huggingFaceChannels } from "../../instrumentation/providers/huggingface-channels"; export const huggingFaceConfigs: InstrumentationConfig[] = [ { diff --git a/js/src/auto-instrumentations/configs/langchain.ts b/js/src/auto-instrumentations/configs/langchain.ts index e8108ed02..c1263a285 100644 --- a/js/src/auto-instrumentations/configs/langchain.ts +++ b/js/src/auto-instrumentations/configs/langchain.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { langChainChannels } from "../../instrumentation/plugins/langchain-channels"; +import { langChainChannels } from "../../instrumentation/providers/langchain-channels"; const langChainCoreVersionRange = ">=0.3.42"; const langChainCallbackManagerFilePath = "dist/callbacks/manager.js"; diff --git a/js/src/auto-instrumentations/configs/langsmith.ts b/js/src/auto-instrumentations/configs/langsmith.ts index 5ecc54f87..0904bc64c 100644 --- a/js/src/auto-instrumentations/configs/langsmith.ts +++ b/js/src/auto-instrumentations/configs/langsmith.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { langSmithChannels } from "../../instrumentation/plugins/langsmith-channels"; +import { langSmithChannels } from "../../instrumentation/providers/langsmith-channels"; const versionRange = ">=0.3.30 <1.0.0"; diff --git a/js/src/auto-instrumentations/configs/mistral.ts b/js/src/auto-instrumentations/configs/mistral.ts index e61fc24be..dab2493c3 100644 --- a/js/src/auto-instrumentations/configs/mistral.ts +++ b/js/src/auto-instrumentations/configs/mistral.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { mistralChannels } from "../../instrumentation/plugins/mistral-channels"; +import { mistralChannels } from "../../instrumentation/providers/mistral-channels"; export const mistralConfigs: InstrumentationConfig[] = [ { diff --git a/js/src/auto-instrumentations/configs/ollama.ts b/js/src/auto-instrumentations/configs/ollama.ts index 5ef28eb64..8403bb652 100644 --- a/js/src/auto-instrumentations/configs/ollama.ts +++ b/js/src/auto-instrumentations/configs/ollama.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { ollamaChannels } from "../../instrumentation/plugins/ollama-channels"; +import { ollamaChannels } from "../../instrumentation/providers/ollama-channels"; const methods = [ ["chat", ollamaChannels.chat.channelName], diff --git a/js/src/auto-instrumentations/configs/openai-agents.ts b/js/src/auto-instrumentations/configs/openai-agents.ts index 3647edbc5..8bf95e99f 100644 --- a/js/src/auto-instrumentations/configs/openai-agents.ts +++ b/js/src/auto-instrumentations/configs/openai-agents.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { openAIAgentsCoreChannels } from "../../instrumentation/plugins/openai-agents-channels"; +import { openAIAgentsCoreChannels } from "../../instrumentation/providers/openai-agents-channels"; const lifecycleMethods = [ ["onTraceStart", openAIAgentsCoreChannels.onTraceStart.channelName], diff --git a/js/src/auto-instrumentations/configs/openai-codex.ts b/js/src/auto-instrumentations/configs/openai-codex.ts index 40ee60784..a725caeb9 100644 --- a/js/src/auto-instrumentations/configs/openai-codex.ts +++ b/js/src/auto-instrumentations/configs/openai-codex.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { openAICodexChannels } from "../../instrumentation/plugins/openai-codex-channels"; +import { openAICodexChannels } from "../../instrumentation/providers/openai-codex-channels"; const openAICodexVersionRange = ">=0.128.0 <1.0.0"; diff --git a/js/src/auto-instrumentations/configs/openai.ts b/js/src/auto-instrumentations/configs/openai.ts index 28d85b7f6..411bd2654 100644 --- a/js/src/auto-instrumentations/configs/openai.ts +++ b/js/src/auto-instrumentations/configs/openai.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { openAIChannels } from "../../instrumentation/plugins/openai-channels"; +import { openAIChannels } from "../../instrumentation/providers/openai-channels"; /** * Instrumentation configurations for the OpenAI SDK. diff --git a/js/src/auto-instrumentations/configs/openrouter-agent.ts b/js/src/auto-instrumentations/configs/openrouter-agent.ts index f9f6191e1..d228c1ed8 100644 --- a/js/src/auto-instrumentations/configs/openrouter-agent.ts +++ b/js/src/auto-instrumentations/configs/openrouter-agent.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { openRouterAgentChannels } from "../../instrumentation/plugins/openrouter-agent-channels"; +import { openRouterAgentChannels } from "../../instrumentation/providers/openrouter-agent-channels"; export const openRouterAgentConfigs: InstrumentationConfig[] = [ { diff --git a/js/src/auto-instrumentations/configs/openrouter.ts b/js/src/auto-instrumentations/configs/openrouter.ts index c7b2ce7e4..2607b3da4 100644 --- a/js/src/auto-instrumentations/configs/openrouter.ts +++ b/js/src/auto-instrumentations/configs/openrouter.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { openRouterChannels } from "../../instrumentation/plugins/openrouter-channels"; +import { openRouterChannels } from "../../instrumentation/providers/openrouter-channels"; export const openRouterConfigs: InstrumentationConfig[] = [ { diff --git a/js/src/auto-instrumentations/configs/pi-coding-agent.ts b/js/src/auto-instrumentations/configs/pi-coding-agent.ts index 0bf6e2a56..3401aec0d 100644 --- a/js/src/auto-instrumentations/configs/pi-coding-agent.ts +++ b/js/src/auto-instrumentations/configs/pi-coding-agent.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { piCodingAgentChannels } from "../../instrumentation/plugins/pi-coding-agent-channels"; +import { piCodingAgentChannels } from "../../instrumentation/providers/pi-coding-agent-channels"; const piCodingAgentVersionRange = ">=0.79.0 <0.82.0"; diff --git a/js/src/auto-instrumentations/configs/strands-agent-sdk.ts b/js/src/auto-instrumentations/configs/strands-agent-sdk.ts index 7a7f7a5b9..f02439a19 100644 --- a/js/src/auto-instrumentations/configs/strands-agent-sdk.ts +++ b/js/src/auto-instrumentations/configs/strands-agent-sdk.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "@apm-js-collab/code-transformer"; -import { strandsAgentSDKChannels } from "../../instrumentation/plugins/strands-agent-sdk-channels"; +import { strandsAgentSDKChannels } from "../../instrumentation/providers/strands-agent-sdk-channels"; const strandsAgentSDKVersionRange = ">=1.0.0 <2.0.0"; diff --git a/js/src/auto-instrumentations/configs/voyageai.ts b/js/src/auto-instrumentations/configs/voyageai.ts index f54edf84e..9fbe886be 100644 --- a/js/src/auto-instrumentations/configs/voyageai.ts +++ b/js/src/auto-instrumentations/configs/voyageai.ts @@ -1,5 +1,5 @@ import type { InstrumentationConfig } from "../orchestrion-js"; -import { voyageAIChannels } from "../../instrumentation/plugins/voyageai-channels"; +import { voyageAIChannels } from "../../instrumentation/providers/voyageai-channels"; const GENERATED_CLIENT_MODULES = [ "dist/cjs/Client.js", diff --git a/js/src/auto-instrumentations/loader/cjs-patch.ts b/js/src/auto-instrumentations/loader/cjs-patch.ts index df5c448ae..d94511563 100644 --- a/js/src/auto-instrumentations/loader/cjs-patch.ts +++ b/js/src/auto-instrumentations/loader/cjs-patch.ts @@ -50,7 +50,7 @@ export class ModulePatch { // Per-package source patches (see loader/special-case-patches.ts). // Anti-pattern intentionally isolated in its own module — do not - // expand inline here; new integrations belong in the standard plugin + // expand inline here; new integrations belong in the standard consumer // pipeline. const patched = applySpecialCasePatch({ packageName, diff --git a/js/src/auto-instrumentations/loader/special-case-patches.ts b/js/src/auto-instrumentations/loader/special-case-patches.ts index f42215592..bf94b5926 100644 --- a/js/src/auto-instrumentations/loader/special-case-patches.ts +++ b/js/src/auto-instrumentations/loader/special-case-patches.ts @@ -6,8 +6,8 @@ * Every entry in this file represents a target SDK that doesn't expose a * stable extension point we can hook through global instrumentation hooks + the * internal Orchestrion matcher. New integrations should - * **prefer the standard channel-handler / `BasePlugin` pattern** used by - * every other integration in `js/src/instrumentation/plugins/*-plugin.ts`. + * **prefer the standard channel-consumer pattern** used by the integrations in + * `js/src/instrumentation/providers/*-plugin.ts`. * Only add an entry here when the target SDK gives us no other option (e.g. * the SDK relies on side-effectful module initialization, exposes its API * exclusively as re-exports from content-hashed chunks, or otherwise can't diff --git a/js/src/exports.ts b/js/src/exports.ts index e8286a959..487ec7430 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -200,7 +200,7 @@ export { BraintrustMiddleware, wrapAISDKModel, } from "./wrappers/ai-sdk"; -export { braintrustEveInstrumentation } from "./instrumentation/plugins/eve-plugin"; +export { braintrustEveInstrumentation } from "./instrumentation"; export { collectAnthropicSession } from "./wrappers/anthropic-session-collector"; export { wrapAnthropic } from "./wrappers/anthropic"; export { diff --git a/js/src/imports.test.ts b/js/src/imports.test.ts index c2d53c767..e00c08223 100644 --- a/js/src/imports.test.ts +++ b/js/src/imports.test.ts @@ -133,11 +133,13 @@ describe("Import restrictions", () => { // Check for dynamic import() statements // Match import(...) but not static import statements - // Exception: allow dynamic import in anthropic-plugin for APIPromise patching + // Exception: allow dynamic import in anthropic-instrumentation for APIPromise patching if ( /\bimport\s*\(/.test(line) && !/^import\s+/.test(line.trim()) && - !relativePath.includes("instrumentation/plugins/anthropic-plugin.ts") + !relativePath.includes( + "instrumentation/providers/anthropic-instrumentation.ts", + ) ) { violations.push( `${relativePath}:${index + 1} - Found dynamic import() statement: "${line.trim()}"`, diff --git a/js/src/instrumentation/README.md b/js/src/instrumentation/README.md index 26487ec26..1fc368594 100644 --- a/js/src/instrumentation/README.md +++ b/js/src/instrumentation/README.md @@ -1,7 +1,8 @@ -# Writing Braintrust Instrumentation Plugins +# Braintrust Instrumentation Consumers -Braintrust instrumentation plugins wrap provider calls through typed invocation -hooks or consume tracing-compatible events from the internal global registry. +Braintrust instrumentation consumers wrap provider calls through typed +invocation hooks or consume tracing-compatible events from the internal global +registry. Auto-instrumented provider code and manual wrappers use the same typed channels, so extraction, stream handling, and span behavior stay aligned. @@ -13,15 +14,15 @@ An instrumentation has four parts: transformation. 2. A typed channel defines its arguments, result, extra event fields, and stable `orchestrion::` identifier. -3. A plugin intercepts that channel, or subscribes to its legacy tracing - lifecycle, and maps the call into Braintrust spans. +3. An internal consumer intercepts that channel, or subscribes to its legacy + tracing lifecycle, and maps the call into Braintrust spans. 4. A manual wrapper invokes the same typed channel when transformation is not available. -The global hook transport is internal. New and migrated plugins should prefer -the typed channel's `intercept` API. Existing plugins can continue using -`traceAsyncChannel`, `traceStreamingChannel`, `traceSyncStreamChannel`, or -`BasePlugin` helpers during the gradual migration. +The global hook transport and its consumers are internal. New and migrated +consumers should prefer the typed channel's `intercept` API. Existing consumers +can continue using `traceAsyncChannel`, `traceStreamingChannel`, or +`traceSyncStreamChannel` during the gradual migration. ## Invocation Hooks @@ -100,27 +101,25 @@ construct it from the package and operation. Prefer the shared tracing helpers: ```ts -this.register( - traceAsyncChannel(providerChannels.create, { - name: "provider.messages.create", - type: "llm", - extractInput(args) { - return { - input: args[0].messages, - metadata: { model: args[0].model }, - }; - }, - extractOutput(result) { - return result.content; - }, - extractMetrics(result) { - return { - prompt_tokens: result.usage.input_tokens, - completion_tokens: result.usage.output_tokens, - }; - }, - }), -); +traceAsyncChannel(providerChannels.create, { + name: "provider.messages.create", + type: "llm", + extractInput(args) { + return { + input: args[0].messages, + metadata: { model: args[0].model }, + }; + }, + extractOutput(result) { + return result.content; + }, + extractMetrics(result) { + return { + prompt_tokens: result.usage.input_tokens, + completion_tokens: result.usage.output_tokens, + }; + }, +}); ``` The helpers: @@ -129,7 +128,7 @@ The helpers: - bind the current span store to `start` for async-context propagation - contain extraction failures and log them through `debugLogger` - patch streams without replacing their public semantics -- unsubscribe and unbind stores when a plugin is disabled +- install process-lifetime subscriptions and span-store bindings Use raw `IsoChannelHandlers` only when a provider requires lifecycle behavior that the shared helpers cannot express. @@ -146,7 +145,7 @@ return providerChannels.create.invoke(originalCreate, this, [params], { ``` Legacy wrappers can continue calling the tracing-compatible operators until -their plugin is migrated: +their consumer is migrated: ```ts return providerChannels.create.tracePromise(() => originalCreate(params), { @@ -155,7 +154,7 @@ return providerChannels.create.tracePromise(() => originalCreate(params), { ``` Do not create spans directly inside wrappers. Keeping span creation in the -plugin prevents auto and manual instrumentation from drifting. +internal consumer prevents auto and manual instrumentation from drifting. ## Promise and Stream Requirements @@ -177,14 +176,14 @@ termination, and async context. - Avoid prototype-sensitive merges and unnecessary mutation of provider data. - Capture only fields permitted by the instrumentation specification. - Pass `Error` objects directly to `span.log({ error })`. -- Use narrow vendored provider interfaces shared by wrappers and plugins. -- Keep enable, disable, subscription, and patching behavior idempotent. +- Use narrow vendored provider interfaces shared by wrappers and consumers. +- Keep enable, subscription, and patching behavior idempotent. ## Testing Test at the narrowest useful layers: -1. Plugin unit tests for extraction and span handling. +1. Consumer unit tests for extraction and span handling. 2. Global hook/runtime tests for lifecycle and context behavior. 3. Orchestrion transformation tests for generated wrappers. 4. Bundler and loader tests for real transformed execution. diff --git a/js/src/instrumentation/auto-instrumentation-suppression.ts b/js/src/instrumentation/auto-instrumentation-suppression.ts index 2b31044ef..6002e0d1f 100644 --- a/js/src/instrumentation/auto-instrumentation-suppression.ts +++ b/js/src/instrumentation/auto-instrumentation-suppression.ts @@ -45,10 +45,10 @@ export function runWithAutoInstrumentationSuppressed(callback: () => R): R { export function bindAutoInstrumentationSuppressionToStart( tracingChannel: Pick, "start">, -): (() => void) | undefined { +): void { const startChannel = tracingChannel.start; if (!startChannel) { - return undefined; + return; } const store = suppressionStore(); @@ -61,10 +61,6 @@ export function bindAutoInstrumentationSuppressionToStart( }, ], })); - - return () => { - startChannel.unbindStore(store); - }; } export function enterAutoInstrumentationAllowed(): () => void { diff --git a/js/src/instrumentation/braintrust-plugin.test.ts b/js/src/instrumentation/braintrust-plugin.test.ts deleted file mode 100644 index 3365ec279..000000000 --- a/js/src/instrumentation/braintrust-plugin.test.ts +++ /dev/null @@ -1,1791 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { BraintrustPlugin } from "./braintrust-plugin"; -import { OpenAIPlugin } from "./plugins/openai-plugin"; -import { OpenAICodexPlugin } from "./plugins/openai-codex-plugin"; -import { AnthropicPlugin } from "./plugins/anthropic-plugin"; -import { AISDKPlugin } from "./plugins/ai-sdk-plugin"; -import { ClaudeAgentSDKPlugin } from "./plugins/claude-agent-sdk-plugin"; -import { CloudflareThinkPlugin } from "./plugins/cloudflare-think-plugin"; -import { OpenAIAgentsPlugin } from "./plugins/openai-agents-plugin"; -import { GoogleGenAIPlugin } from "./plugins/google-genai-plugin"; -import { HuggingFacePlugin } from "./plugins/huggingface-plugin"; -import { HuggingFaceTransformersPlugin } from "./plugins/huggingface-transformers-plugin"; -import { OpenRouterAgentPlugin } from "./plugins/openrouter-agent-plugin"; -import { OpenRouterPlugin } from "./plugins/openrouter-plugin"; -import { MistralPlugin } from "./plugins/mistral-plugin"; -import { OllamaPlugin } from "./plugins/ollama-plugin"; -import { CoherePlugin } from "./plugins/cohere-plugin"; -import { GroqPlugin } from "./plugins/groq-plugin"; -import { GitHubCopilotPlugin } from "./plugins/github-copilot-plugin"; -import { LangChainPlugin } from "./plugins/langchain-plugin"; -import { LangSmithPlugin } from "./plugins/langsmith-plugin"; -import { PiCodingAgentPlugin } from "./plugins/pi-coding-agent-plugin"; -import { StrandsAgentSDKPlugin } from "./plugins/strands-agent-sdk-plugin"; -import { VoyageAIPlugin } from "./plugins/voyageai-plugin"; -import { CloudflareAIChatPlugin } from "./plugins/cloudflare-ai-chat-plugin"; -import { CloudflareAgentsPlugin } from "./plugins/cloudflare-agents-plugin"; - -function createPluginClassMock() { - return vi.fn(function MockPlugin(this: { - enable: ReturnType; - disable: ReturnType; - }) { - this.enable = vi.fn(); - this.disable = vi.fn(); - }); -} - -// Preserve the re-exported utility functions (parseMetricsFromUsage, etc.) -// while mocking out the OpenAIPlugin class — those utilities are also -// imported and tested at the bottom of this file via braintrust-plugin's -// re-exports. -vi.mock("./plugins/openai-plugin", async () => { - const actual = await vi.importActual< - typeof import("./plugins/openai-plugin") - >("./plugins/openai-plugin"); - return { - ...actual, - OpenAIPlugin: createPluginClassMock(), - }; -}); - -vi.mock("./plugins/anthropic-plugin", () => ({ - AnthropicPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/openai-codex-plugin", () => ({ - OpenAICodexPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/ai-sdk-plugin", () => ({ - AISDKPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/claude-agent-sdk-plugin", () => ({ - ClaudeAgentSDKPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/cloudflare-think-plugin", () => ({ - CloudflareThinkPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/openai-agents-plugin", () => ({ - OpenAIAgentsPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/google-genai-plugin", () => ({ - GoogleGenAIPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/huggingface-plugin", () => ({ - HuggingFacePlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/huggingface-transformers-plugin", () => ({ - HuggingFaceTransformersPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/openrouter-plugin", () => ({ - OpenRouterPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/openrouter-agent-plugin", () => ({ - OpenRouterAgentPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/mistral-plugin", () => ({ - MistralPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/ollama-plugin", () => ({ - OllamaPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/cohere-plugin", () => ({ - CoherePlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/groq-plugin", () => ({ - GroqPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/github-copilot-plugin", () => ({ - GitHubCopilotPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/langchain-plugin", () => ({ - LangChainPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/langsmith-plugin", () => ({ - LangSmithPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/pi-coding-agent-plugin", () => ({ - PiCodingAgentPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/strands-agent-sdk-plugin", () => ({ - StrandsAgentSDKPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/voyageai-plugin", () => ({ - VoyageAIPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/cloudflare-ai-chat-plugin", () => ({ - CloudflareAIChatPlugin: createPluginClassMock(), -})); - -vi.mock("./plugins/cloudflare-agents-plugin", () => ({ - CloudflareAgentsPlugin: createPluginClassMock(), -})); - -describe("BraintrustPlugin", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("sub-plugin creation (default behavior)", () => { - it("should create and enable OpenAI plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(OpenAIPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Anthropic plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(AnthropicPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable OpenAI Codex plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(OpenAICodexPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(OpenAICodexPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable AI SDK plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(AISDKPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Claude Agent SDK plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - const mockInstance = - vi.mocked(ClaudeAgentSDKPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Cloudflare Think plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(CloudflareThinkPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(CloudflareThinkPlugin).mock.results[0] - .value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable OpenAI Agents plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(OpenAIAgentsPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Google GenAI plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(GoogleGenAIPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable HuggingFace plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(HuggingFacePlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - expect(HuggingFaceTransformersPlugin).toHaveBeenCalledTimes(1); - const transformersMockInstance = vi.mocked(HuggingFaceTransformersPlugin) - .mock.results[0].value; - expect(transformersMockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable OpenRouter plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(OpenRouterPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable OpenRouter Agent plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(OpenRouterAgentPlugin).mock.results[0] - .value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Mistral plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(MistralPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(MistralPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Cohere plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(CoherePlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(CoherePlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Voyage AI plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(VoyageAIPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(VoyageAIPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Groq plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(GroqPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(GroqPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable GitHubCopilot plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(GitHubCopilotPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(GitHubCopilotPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable LangChain plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(LangChainPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(LangChainPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Strands Agent SDK plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(StrandsAgentSDKPlugin).mock.results[0] - .value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Cloudflare AI Chat plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(CloudflareAIChatPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(CloudflareAIChatPlugin).mock.results[0] - .value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create and enable Cloudflare Agents plugin by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(CloudflareAgentsPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(CloudflareAgentsPlugin).mock.results[0] - .value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should create all plugins when enabled with no config", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenAICodexPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - expect(CoherePlugin).toHaveBeenCalledTimes(1); - expect(GroqPlugin).toHaveBeenCalledTimes(1); - expect(GitHubCopilotPlugin).toHaveBeenCalledTimes(1); - expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(LangChainPlugin).toHaveBeenCalledTimes(1); - }); - - it("should create all plugins when enabled with empty config", () => { - const plugin = new BraintrustPlugin({}); - plugin.enable(); - - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenAICodexPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - expect(CoherePlugin).toHaveBeenCalledTimes(1); - expect(GroqPlugin).toHaveBeenCalledTimes(1); - expect(GitHubCopilotPlugin).toHaveBeenCalledTimes(1); - expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(LangChainPlugin).toHaveBeenCalledTimes(1); - }); - - it("should create all plugins when enabled with empty integrations config", () => { - const plugin = new BraintrustPlugin({ integrations: {} }); - plugin.enable(); - - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenAICodexPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - expect(CoherePlugin).toHaveBeenCalledTimes(1); - expect(GroqPlugin).toHaveBeenCalledTimes(1); - expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(LangChainPlugin).toHaveBeenCalledTimes(1); - }); - }); - - describe("config-based disabling", () => { - it("should not create OpenAI plugin when openai: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { openai: false }, - }); - plugin.enable(); - - expect(OpenAIPlugin).not.toHaveBeenCalled(); - // Other plugins should still be created - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Anthropic plugin when anthropic: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { anthropic: false }, - }); - plugin.enable(); - - expect(AnthropicPlugin).not.toHaveBeenCalled(); - // Other plugins should still be created - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenAICodexPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create OpenAI Codex plugin when openaiCodexSDK: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { openaiCodexSDK: false }, - }); - plugin.enable(); - - expect(OpenAICodexPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create AI SDK plugin when aisdk: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { aisdk: false }, - }); - plugin.enable(); - - expect(AISDKPlugin).not.toHaveBeenCalled(); - // Other plugins should still be created - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Claude Agent SDK plugin when claudeAgentSDK: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { claudeAgentSDK: false }, - }); - plugin.enable(); - - expect(ClaudeAgentSDKPlugin).not.toHaveBeenCalled(); - // Other plugins should still be created - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Cloudflare Think plugin when cloudflareThink: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { cloudflareThink: false }, - }); - plugin.enable(); - - expect(CloudflareThinkPlugin).not.toHaveBeenCalled(); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create OpenAI Agents plugin when openAIAgents: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { openAIAgents: false }, - }); - plugin.enable(); - - expect(OpenAIAgentsPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create HuggingFace plugin when huggingface: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { huggingface: false }, - }); - plugin.enable(); - - expect(HuggingFacePlugin).not.toHaveBeenCalled(); - expect(HuggingFaceTransformersPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Google GenAI plugin when googleGenAI: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { googleGenAI: false }, - }); - plugin.enable(); - - expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); - // Other plugins should still be created - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create OpenRouter plugin when openrouter: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { openrouter: false }, - }); - plugin.enable(); - - expect(OpenRouterPlugin).not.toHaveBeenCalled(); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Mistral plugin when mistral: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { mistral: false }, - }); - plugin.enable(); - - expect(MistralPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Ollama plugin when ollama: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { ollama: false }, - }); - plugin.enable(); - - expect(OllamaPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Cohere plugin when cohere: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { cohere: false }, - }); - plugin.enable(); - - expect(CoherePlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Groq plugin when groq: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { groq: false }, - }); - plugin.enable(); - - expect(GroqPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create GitHubCopilot plugin when gitHubCopilot: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { gitHubCopilot: false }, - }); - plugin.enable(); - - expect(GitHubCopilotPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(GroqPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create LangChain plugin when langchain: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { langchain: false }, - }); - plugin.enable(); - - expect(LangChainPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - }); - - it("should create LangSmith with LangChain deduplication by default", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - expect(LangSmithPlugin).toHaveBeenCalledWith({ - skipLangChainRuns: true, - }); - const mockInstance = vi.mocked(LangSmithPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should disable LangSmith or let it capture LangChain runs", () => { - const disabled = new BraintrustPlugin({ - integrations: { langsmith: false }, - }); - disabled.enable(); - expect(LangSmithPlugin).not.toHaveBeenCalled(); - - const withoutLangChain = new BraintrustPlugin({ - integrations: { langchain: false }, - }); - withoutLangChain.enable(); - expect(LangSmithPlugin).toHaveBeenCalledWith({ - skipLangChainRuns: false, - }); - }); - - it("should not create OpenRouter Agent plugin when openrouterAgent: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { openrouterAgent: false }, - }); - plugin.enable(); - - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).not.toHaveBeenCalled(); - }); - - it("should not create any plugins when all are disabled", () => { - const plugin = new BraintrustPlugin({ - integrations: { - openai: false, - openaiCodexSDK: false, - anthropic: false, - aisdk: false, - claudeAgentSDK: false, - cloudflareThink: false, - openAIAgents: false, - googleGenAI: false, - huggingface: false, - openrouter: false, - openrouterAgent: false, - mistral: false, - ollama: false, - cohere: false, - groq: false, - gitHubCopilot: false, - langchain: false, - langsmith: false, - piCodingAgent: false, - strandsAgentSDK: false, - cloudflareAIChat: false, - cloudflareAgents: false, - }, - }); - plugin.enable(); - - expect(OpenAIPlugin).not.toHaveBeenCalled(); - expect(OpenAICodexPlugin).not.toHaveBeenCalled(); - expect(AnthropicPlugin).not.toHaveBeenCalled(); - expect(AISDKPlugin).not.toHaveBeenCalled(); - expect(ClaudeAgentSDKPlugin).not.toHaveBeenCalled(); - expect(CloudflareThinkPlugin).not.toHaveBeenCalled(); - expect(OpenAIAgentsPlugin).not.toHaveBeenCalled(); - expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); - expect(HuggingFacePlugin).not.toHaveBeenCalled(); - expect(OpenRouterPlugin).not.toHaveBeenCalled(); - expect(OpenRouterAgentPlugin).not.toHaveBeenCalled(); - expect(MistralPlugin).not.toHaveBeenCalled(); - expect(OllamaPlugin).not.toHaveBeenCalled(); - expect(CoherePlugin).not.toHaveBeenCalled(); - expect(GroqPlugin).not.toHaveBeenCalled(); - expect(GitHubCopilotPlugin).not.toHaveBeenCalled(); - expect(LangChainPlugin).not.toHaveBeenCalled(); - expect(LangSmithPlugin).not.toHaveBeenCalled(); - expect(PiCodingAgentPlugin).not.toHaveBeenCalled(); - expect(StrandsAgentSDKPlugin).not.toHaveBeenCalled(); - expect(CloudflareAIChatPlugin).not.toHaveBeenCalled(); - expect(CloudflareAgentsPlugin).not.toHaveBeenCalled(); - }); - - it("should not create Pi Coding Agent plugin when piCodingAgent: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { piCodingAgent: false }, - }); - plugin.enable(); - - expect(PiCodingAgentPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Strands Agent SDK plugin when strandsAgentSDK: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { strandsAgentSDK: false }, - }); - plugin.enable(); - - expect(StrandsAgentSDKPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Cloudflare AI Chat plugin when cloudflareAIChat: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { cloudflareAIChat: false }, - }); - plugin.enable(); - - expect(CloudflareAIChatPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Cloudflare Agents plugin when cloudflareAgents: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { cloudflareAgents: false }, - }); - plugin.enable(); - - expect(CloudflareAgentsPlugin).not.toHaveBeenCalled(); - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - }); - - it("should allow selective enabling of plugins", () => { - const plugin = new BraintrustPlugin({ - integrations: { - openai: true, - anthropic: false, - aisdk: false, - claudeAgentSDK: true, - openAIAgents: true, - googleGenAI: false, - huggingface: true, - openrouter: true, - mistral: false, - }, - }); - plugin.enable(); - - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).not.toHaveBeenCalled(); - expect(AISDKPlugin).not.toHaveBeenCalled(); - expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); - expect(MistralPlugin).not.toHaveBeenCalled(); - }); - }); - - describe("backward compatibility (legacy config keys)", () => { - it("should not create AI SDK plugin when vercel: false (legacy)", () => { - const plugin = new BraintrustPlugin({ - integrations: { vercel: false }, - }); - plugin.enable(); - - expect(AISDKPlugin).not.toHaveBeenCalled(); - // Other plugins should still be created - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create Google GenAI plugin when google: false (legacy)", () => { - const plugin = new BraintrustPlugin({ - integrations: { google: false }, - }); - plugin.enable(); - - expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); - // Other plugins should still be created - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - }); - - it("should not create LangChain plugin when langgraph: false (alias)", () => { - const plugin = new BraintrustPlugin({ - integrations: { langgraph: false }, - }); - plugin.enable(); - - expect(LangChainPlugin).not.toHaveBeenCalled(); - expect(LangSmithPlugin).toHaveBeenCalledWith({ - skipLangChainRuns: true, - }); - }); - - it("should not create AI SDK plugin when both aisdk and vercel are false", () => { - const plugin = new BraintrustPlugin({ - integrations: { aisdk: false, vercel: false }, - }); - plugin.enable(); - - expect(AISDKPlugin).not.toHaveBeenCalled(); - }); - - it("should not create Google GenAI plugin when both googleGenAI and google are false", () => { - const plugin = new BraintrustPlugin({ - integrations: { googleGenAI: false, google: false }, - }); - plugin.enable(); - - expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); - }); - - it("should not create AI SDK plugin when aisdk is true but vercel is false", () => { - const plugin = new BraintrustPlugin({ - integrations: { aisdk: true, vercel: false }, - }); - plugin.enable(); - - expect(AISDKPlugin).not.toHaveBeenCalled(); - }); - - it("should not create Google GenAI plugin when googleGenAI is true but google is false", () => { - const plugin = new BraintrustPlugin({ - integrations: { googleGenAI: true, google: false }, - }); - plugin.enable(); - - expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); - }); - - it("should create AI SDK plugin when vercel is true and aisdk is not set", () => { - const plugin = new BraintrustPlugin({ - integrations: { vercel: true }, - }); - plugin.enable(); - - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - }); - - it("should create Google GenAI plugin when google is true and googleGenAI is not set", () => { - const plugin = new BraintrustPlugin({ - integrations: { google: true }, - }); - plugin.enable(); - - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - }); - }); - - describe("enable/disable lifecycle", () => { - it("should enable all sub-plugins when enabled", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - const openaiMock = vi.mocked(OpenAIPlugin).mock.results[0].value; - const openAICodexMock = - vi.mocked(OpenAICodexPlugin).mock.results[0].value; - const anthropicMock = vi.mocked(AnthropicPlugin).mock.results[0].value; - const aiSDKMock = vi.mocked(AISDKPlugin).mock.results[0].value; - const claudeAgentSDKMock = - vi.mocked(ClaudeAgentSDKPlugin).mock.results[0].value; - const openAIAgentsMock = - vi.mocked(OpenAIAgentsPlugin).mock.results[0].value; - const googleGenAIMock = - vi.mocked(GoogleGenAIPlugin).mock.results[0].value; - const huggingFaceMock = - vi.mocked(HuggingFacePlugin).mock.results[0].value; - const huggingFaceTransformersMock = vi.mocked( - HuggingFaceTransformersPlugin, - ).mock.results[0].value; - const openRouterMock = vi.mocked(OpenRouterPlugin).mock.results[0].value; - const openRouterAgentMock = vi.mocked(OpenRouterAgentPlugin).mock - .results[0].value; - const mistralMock = vi.mocked(MistralPlugin).mock.results[0].value; - const ollamaMock = vi.mocked(OllamaPlugin).mock.results[0].value; - const cohereMock = vi.mocked(CoherePlugin).mock.results[0].value; - const groqMock = vi.mocked(GroqPlugin).mock.results[0].value; - const piCodingAgentMock = - vi.mocked(PiCodingAgentPlugin).mock.results[0].value; - const strandsAgentSDKMock = vi.mocked(StrandsAgentSDKPlugin).mock - .results[0].value; - const cloudflareAgentsMock = vi.mocked(CloudflareAgentsPlugin).mock - .results[0].value; - const langChainMock = vi.mocked(LangChainPlugin).mock.results[0].value; - - expect(openaiMock.enable).toHaveBeenCalledTimes(1); - expect(openAICodexMock.enable).toHaveBeenCalledTimes(1); - expect(anthropicMock.enable).toHaveBeenCalledTimes(1); - expect(aiSDKMock.enable).toHaveBeenCalledTimes(1); - expect(claudeAgentSDKMock.enable).toHaveBeenCalledTimes(1); - expect(openAIAgentsMock.enable).toHaveBeenCalledTimes(1); - expect(googleGenAIMock.enable).toHaveBeenCalledTimes(1); - expect(huggingFaceMock.enable).toHaveBeenCalledTimes(1); - expect(huggingFaceTransformersMock.enable).toHaveBeenCalledTimes(1); - expect(openRouterMock.enable).toHaveBeenCalledTimes(1); - expect(openRouterAgentMock.enable).toHaveBeenCalledTimes(1); - expect(mistralMock.enable).toHaveBeenCalledTimes(1); - expect(ollamaMock.enable).toHaveBeenCalledTimes(1); - expect(cohereMock.enable).toHaveBeenCalledTimes(1); - expect(groqMock.enable).toHaveBeenCalledTimes(1); - expect(piCodingAgentMock.enable).toHaveBeenCalledTimes(1); - expect(strandsAgentSDKMock.enable).toHaveBeenCalledTimes(1); - expect(cloudflareAgentsMock.enable).toHaveBeenCalledTimes(1); - expect(langChainMock.enable).toHaveBeenCalledTimes(1); - }); - - it("should disable and nullify all sub-plugins when disabled", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - const openaiMock = vi.mocked(OpenAIPlugin).mock.results[0].value; - const openAICodexMock = - vi.mocked(OpenAICodexPlugin).mock.results[0].value; - const anthropicMock = vi.mocked(AnthropicPlugin).mock.results[0].value; - const aiSDKMock = vi.mocked(AISDKPlugin).mock.results[0].value; - const claudeAgentSDKMock = - vi.mocked(ClaudeAgentSDKPlugin).mock.results[0].value; - const openAIAgentsMock = - vi.mocked(OpenAIAgentsPlugin).mock.results[0].value; - const googleGenAIMock = - vi.mocked(GoogleGenAIPlugin).mock.results[0].value; - const huggingFaceMock = - vi.mocked(HuggingFacePlugin).mock.results[0].value; - const huggingFaceTransformersMock = vi.mocked( - HuggingFaceTransformersPlugin, - ).mock.results[0].value; - const openRouterMock = vi.mocked(OpenRouterPlugin).mock.results[0].value; - const openRouterAgentMock = vi.mocked(OpenRouterAgentPlugin).mock - .results[0].value; - const mistralMock = vi.mocked(MistralPlugin).mock.results[0].value; - const ollamaMock = vi.mocked(OllamaPlugin).mock.results[0].value; - const cohereMock = vi.mocked(CoherePlugin).mock.results[0].value; - const groqMock = vi.mocked(GroqPlugin).mock.results[0].value; - const piCodingAgentMock = - vi.mocked(PiCodingAgentPlugin).mock.results[0].value; - const strandsAgentSDKMock = vi.mocked(StrandsAgentSDKPlugin).mock - .results[0].value; - const cloudflareAgentsMock = vi.mocked(CloudflareAgentsPlugin).mock - .results[0].value; - const langChainMock = vi.mocked(LangChainPlugin).mock.results[0].value; - - plugin.disable(); - - expect(openaiMock.disable).toHaveBeenCalledTimes(1); - expect(openAICodexMock.disable).toHaveBeenCalledTimes(1); - expect(anthropicMock.disable).toHaveBeenCalledTimes(1); - expect(aiSDKMock.disable).toHaveBeenCalledTimes(1); - expect(claudeAgentSDKMock.disable).toHaveBeenCalledTimes(1); - expect(openAIAgentsMock.disable).toHaveBeenCalledTimes(1); - expect(googleGenAIMock.disable).toHaveBeenCalledTimes(1); - expect(huggingFaceMock.disable).toHaveBeenCalledTimes(1); - expect(huggingFaceTransformersMock.disable).toHaveBeenCalledTimes(1); - expect(openRouterMock.disable).toHaveBeenCalledTimes(1); - expect(openRouterAgentMock.disable).toHaveBeenCalledTimes(1); - expect(mistralMock.disable).toHaveBeenCalledTimes(1); - expect(ollamaMock.disable).toHaveBeenCalledTimes(1); - expect(cohereMock.disable).toHaveBeenCalledTimes(1); - expect(groqMock.disable).toHaveBeenCalledTimes(1); - expect(piCodingAgentMock.disable).toHaveBeenCalledTimes(1); - expect(strandsAgentSDKMock.disable).toHaveBeenCalledTimes(1); - expect(cloudflareAgentsMock.disable).toHaveBeenCalledTimes(1); - expect(langChainMock.disable).toHaveBeenCalledTimes(1); - }); - - it("should be idempotent on multiple enable calls", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - plugin.enable(); - plugin.enable(); - - // Should only create and enable plugins once - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - const mockInstance = vi.mocked(OpenAIPlugin).mock.results[0].value; - expect(mockInstance.enable).toHaveBeenCalledTimes(1); - }); - - it("should be idempotent on multiple disable calls", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - - const openaiMock = vi.mocked(OpenAIPlugin).mock.results[0].value; - - plugin.disable(); - plugin.disable(); - plugin.disable(); - - // Should only disable plugins once - expect(openaiMock.disable).toHaveBeenCalledTimes(1); - }); - - it("should not disable plugins if never enabled", () => { - const plugin = new BraintrustPlugin(); - plugin.disable(); - - // Should not create any plugins - expect(OpenAIPlugin).not.toHaveBeenCalled(); - expect(OpenAICodexPlugin).not.toHaveBeenCalled(); - expect(AnthropicPlugin).not.toHaveBeenCalled(); - expect(AISDKPlugin).not.toHaveBeenCalled(); - expect(ClaudeAgentSDKPlugin).not.toHaveBeenCalled(); - expect(OpenAIAgentsPlugin).not.toHaveBeenCalled(); - expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); - expect(HuggingFacePlugin).not.toHaveBeenCalled(); - expect(OpenRouterPlugin).not.toHaveBeenCalled(); - expect(OpenRouterAgentPlugin).not.toHaveBeenCalled(); - expect(MistralPlugin).not.toHaveBeenCalled(); - expect(OllamaPlugin).not.toHaveBeenCalled(); - expect(CoherePlugin).not.toHaveBeenCalled(); - expect(GroqPlugin).not.toHaveBeenCalled(); - expect(PiCodingAgentPlugin).not.toHaveBeenCalled(); - expect(StrandsAgentSDKPlugin).not.toHaveBeenCalled(); - }); - - it("should allow re-enabling after disable", () => { - const plugin = new BraintrustPlugin(); - plugin.enable(); - plugin.disable(); - - vi.clearAllMocks(); - - plugin.enable(); - - expect(OpenAIPlugin).toHaveBeenCalledTimes(1); - expect(OpenAICodexPlugin).toHaveBeenCalledTimes(1); - expect(AnthropicPlugin).toHaveBeenCalledTimes(1); - expect(AISDKPlugin).toHaveBeenCalledTimes(1); - expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); - expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); - expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); - expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); - expect(MistralPlugin).toHaveBeenCalledTimes(1); - expect(OllamaPlugin).toHaveBeenCalledTimes(1); - expect(CoherePlugin).toHaveBeenCalledTimes(1); - expect(GroqPlugin).toHaveBeenCalledTimes(1); - expect(PiCodingAgentPlugin).toHaveBeenCalledTimes(1); - expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); - expect(LangChainPlugin).toHaveBeenCalledTimes(1); - expect(LangSmithPlugin).toHaveBeenCalledTimes(1); - }); - - it("should only disable plugins that were enabled", () => { - const plugin = new BraintrustPlugin({ - integrations: { - openai: true, - anthropic: false, - aisdk: true, - claudeAgentSDK: false, - openAIAgents: true, - googleGenAI: true, - huggingface: true, - openrouter: true, - openrouterAgent: true, - mistral: false, - ollama: false, - cohere: false, - groq: true, - langchain: true, - }, - }); - plugin.enable(); - - const openaiMock = vi.mocked(OpenAIPlugin).mock.results[0].value; - const aiSDKMock = vi.mocked(AISDKPlugin).mock.results[0].value; - const openAIAgentsMock = - vi.mocked(OpenAIAgentsPlugin).mock.results[0].value; - const googleGenAIMock = - vi.mocked(GoogleGenAIPlugin).mock.results[0].value; - const huggingFaceMock = - vi.mocked(HuggingFacePlugin).mock.results[0].value; - const openRouterMock = vi.mocked(OpenRouterPlugin).mock.results[0].value; - const openRouterAgentMock = vi.mocked(OpenRouterAgentPlugin).mock - .results[0].value; - const groqMock = vi.mocked(GroqPlugin).mock.results[0].value; - const langChainMock = vi.mocked(LangChainPlugin).mock.results[0].value; - - plugin.disable(); - - expect(openaiMock.disable).toHaveBeenCalledTimes(1); - expect(aiSDKMock.disable).toHaveBeenCalledTimes(1); - expect(openAIAgentsMock.disable).toHaveBeenCalledTimes(1); - expect(googleGenAIMock.disable).toHaveBeenCalledTimes(1); - expect(huggingFaceMock.disable).toHaveBeenCalledTimes(1); - expect(openRouterMock.disable).toHaveBeenCalledTimes(1); - expect(openRouterAgentMock.disable).toHaveBeenCalledTimes(1); - expect(groqMock.disable).toHaveBeenCalledTimes(1); - expect(langChainMock.disable).toHaveBeenCalledTimes(1); - expect(MistralPlugin).not.toHaveBeenCalled(); - expect(OllamaPlugin).not.toHaveBeenCalled(); - expect(CoherePlugin).not.toHaveBeenCalled(); - }); - - it("should not create Voyage AI plugin when voyageai: false", () => { - const plugin = new BraintrustPlugin({ - integrations: { voyageai: false }, - }); - plugin.enable(); - - expect(VoyageAIPlugin).not.toHaveBeenCalled(); - }); - }); -}); - -// Re-exported utility function tests from OpenAI plugin -import { - parseMetricsFromUsage, - processImagesInOutput, - aggregateChatCompletionChunks, -} from "./braintrust-plugin"; -import { Attachment } from "../logger"; - -describe("parseMetricsFromUsage", () => { - it("should return empty object for null usage", () => { - expect(parseMetricsFromUsage(null)).toEqual({}); - }); - - it("should return empty object for undefined usage", () => { - expect(parseMetricsFromUsage(undefined)).toEqual({}); - }); - - it("should parse legacy token names", () => { - const usage = { - prompt_tokens: 10, - completion_tokens: 20, - total_tokens: 30, - }; - - const result = parseMetricsFromUsage(usage); - - expect(result).toEqual({ - prompt_tokens: 10, - completion_tokens: 20, - tokens: 30, // total_tokens is mapped to tokens - }); - }); - - it("should parse new API token names and map them", () => { - const usage = { - input_tokens: 15, - output_tokens: 25, - total_tokens: 40, - }; - - const result = parseMetricsFromUsage(usage); - - expect(result).toEqual({ - prompt_tokens: 15, // input_tokens mapped to prompt_tokens - completion_tokens: 25, // output_tokens mapped to completion_tokens - tokens: 40, // total_tokens mapped to tokens - }); - }); - - it("should parse token details fields", () => { - const usage = { - input_tokens: 100, - input_tokens_details: { - cached_tokens: 50, - audio_tokens: 10, - }, - output_tokens: 80, - output_tokens_details: { - reasoning_tokens: 20, - }, - }; - - const result = parseMetricsFromUsage(usage); - - expect(result).toEqual({ - prompt_tokens: 100, - prompt_cached_tokens: 50, // input -> prompt prefix - prompt_audio_tokens: 10, - completion_tokens: 80, - completion_reasoning_tokens: 20, // output -> completion prefix - }); - }); - - it("should handle mixed legacy and new token names", () => { - const usage = { - prompt_tokens: 10, - input_tokens: 15, - completion_tokens: 20, - total_tokens: 30, - }; - - const result = parseMetricsFromUsage(usage); - - expect(result).toEqual({ - prompt_tokens: 15, // Last one wins (input_tokens mapped) - completion_tokens: 20, - tokens: 30, // total_tokens is mapped to tokens - }); - }); - - it("should ignore non-number token values", () => { - const usage = { - prompt_tokens: "not a number", - completion_tokens: 20, - total_tokens: null, - }; - - const result = parseMetricsFromUsage(usage); - - expect(result).toEqual({ - completion_tokens: 20, - }); - }); - - it("should ignore non-object token details", () => { - const usage = { - input_tokens: 100, - input_tokens_details: "not an object", - output_tokens: 80, - }; - - const result = parseMetricsFromUsage(usage); - - expect(result).toEqual({ - prompt_tokens: 100, - completion_tokens: 80, - }); - }); - - it("should ignore non-number values in token details", () => { - const usage = { - input_tokens: 100, - input_tokens_details: { - cached_tokens: 50, - invalid: "not a number", - also_invalid: null, - }, - }; - - const result = parseMetricsFromUsage(usage); - - expect(result).toEqual({ - prompt_tokens: 100, - prompt_cached_tokens: 50, - }); - }); - - it("should handle empty usage object", () => { - expect(parseMetricsFromUsage({})).toEqual({}); - }); - - it("should handle unknown token prefix in details", () => { - const usage = { - custom_tokens: 100, - custom_tokens_details: { - special: 25, - }, - }; - - const result = parseMetricsFromUsage(usage); - - expect(result).toEqual({ - custom_tokens: 100, - custom_special: 25, // custom prefix preserved - }); - }); -}); - -describe("processImagesInOutput", () => { - it("should convert image_generation_call to attachment", () => { - // Create a small 1x1 red PNG base64 - const base64Image = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; - - const output = { - type: "image_generation_call", - result: base64Image, - output_format: "png", - revised_prompt: "A red pixel", - }; - - const result = processImagesInOutput(output); - - expect(result.type).toBe("image_generation_call"); - expect(result.output_format).toBe("png"); - expect(result.revised_prompt).toBe("A red pixel"); - expect(result.result).toBeInstanceOf(Attachment); - - const attachment = result.result as Attachment; - expect(attachment.reference.filename).toContain(".png"); - expect(attachment.reference.content_type).toBe("image/png"); - }); - - it("should handle image_generation_call with long revised_prompt", () => { - const longPrompt = - "This is a very long prompt that should be truncated to 50 characters when used as filename"; - const base64Image = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; - - const output = { - type: "image_generation_call", - result: base64Image, - revised_prompt: longPrompt, - }; - - const result = processImagesInOutput(output); - - const attachment = result.result as Attachment; - // Should be truncated to 50 chars + sanitized + extension - expect(attachment.reference.filename.length).toBeLessThanOrEqual(54); // 50 + ".png" - }); - - it("should handle image_generation_call without revised_prompt", () => { - const base64Image = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; - - const output = { - type: "image_generation_call", - result: base64Image, - output_format: "jpg", - }; - - const result = processImagesInOutput(output); - - const attachment = result.result as Attachment; - expect(attachment.reference.filename).toBe("generated_image.jpg"); - expect(attachment.reference.content_type).toBe("image/jpg"); - }); - - it("should use png as default format", () => { - const base64Image = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; - - const output = { - type: "image_generation_call", - result: base64Image, - }; - - const result = processImagesInOutput(output); - - const attachment = result.result as Attachment; - expect(attachment.reference.filename).toBe("generated_image.png"); - expect(attachment.reference.content_type).toBe("image/png"); - }); - - it("should process arrays recursively", () => { - const base64Image = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; - - const output = [ - { type: "text", content: "Hello" }, - { - type: "image_generation_call", - result: base64Image, - }, - { type: "text", content: "World" }, - ]; - - const result = processImagesInOutput(output); - - expect(Array.isArray(result)).toBe(true); - expect(result[0]).toEqual({ type: "text", content: "Hello" }); - expect(result[1].result).toBeInstanceOf(Attachment); - expect(result[2]).toEqual({ type: "text", content: "World" }); - }); - - it("should pass through non-image objects unchanged", () => { - const output = { - type: "text", - content: "Hello world", - }; - - const result = processImagesInOutput(output); - expect(result).toEqual(output); - }); - - it("should pass through primitive values unchanged", () => { - expect(processImagesInOutput("string")).toBe("string"); - expect(processImagesInOutput(42)).toBe(42); - expect(processImagesInOutput(true)).toBe(true); - expect(processImagesInOutput(null)).toBe(null); - }); - - it("should not process image_generation_call with non-string result", () => { - const output = { - type: "image_generation_call", - result: 12345, // Not a string - }; - - const result = processImagesInOutput(output); - expect(result).toEqual(output); - expect(result.result).toBe(12345); - }); -}); - -describe("aggregateChatCompletionChunks", () => { - it("should aggregate simple text chunks", () => { - const chunks = [ - { - choices: [{ delta: { role: "assistant", content: "Hello" } }], - }, - { - choices: [{ delta: { content: " world" } }], - }, - { - choices: [{ delta: { content: "!" } }], - }, - ]; - - const result = aggregateChatCompletionChunks(chunks as any); - - expect(result.output).toEqual([ - { - index: 0, - message: { - role: "assistant", - content: "Hello world!", - tool_calls: undefined, - }, - logprobs: null, - finish_reason: undefined, - }, - ]); - expect(result.metrics).toEqual({}); - }); - - it("should extract role from first chunk", () => { - const chunks = [ - { - choices: [{ delta: { role: "assistant" } }], - }, - { - choices: [{ delta: { content: "Hi" } }], - }, - ]; - - const result = aggregateChatCompletionChunks(chunks as any); - - expect(result.output[0].message.role).toBe("assistant"); - }); - - it("should extract finish_reason from last chunk with it", () => { - const chunks = [ - { - choices: [{ delta: { role: "assistant", content: "Done" } }], - }, - { - choices: [{ delta: { finish_reason: "stop" } }], - }, - ]; - - const result = aggregateChatCompletionChunks(chunks); - - expect(result.output[0].finish_reason).toBe("stop"); - }); - - it("should aggregate tool calls by id", () => { - const chunks = [ - { - choices: [ - { - delta: { - role: "assistant", - tool_calls: [ - { - id: "call_1", - type: "function", - function: { name: "get_weather", arguments: '{"loc' }, - }, - ], - }, - }, - ], - }, - { - choices: [ - { - delta: { - tool_calls: [ - { - function: { arguments: 'ation":"' }, - }, - ], - }, - }, - ], - }, - { - choices: [ - { - delta: { - tool_calls: [ - { - function: { arguments: 'NYC"}' }, - }, - ], - }, - }, - ], - }, - ]; - - const result = aggregateChatCompletionChunks(chunks); - - expect(result.output[0].message.tool_calls).toEqual([ - { - id: "call_1", - type: "function", - function: { name: "get_weather", arguments: '{"location":"NYC"}' }, - }, - ]); - }); - - it("should handle multiple tool calls", () => { - const chunks = [ - { - choices: [ - { - delta: { - role: "assistant", - tool_calls: [ - { - id: "call_1", - type: "function", - function: { name: "tool1", arguments: '{"a":' }, - }, - ], - }, - }, - ], - }, - { - choices: [ - { - delta: { - tool_calls: [ - { - function: { arguments: "1}" }, - }, - ], - }, - }, - ], - }, - { - choices: [ - { - delta: { - tool_calls: [ - { - id: "call_2", - type: "function", - function: { name: "tool2", arguments: '{"b":' }, - }, - ], - }, - }, - ], - }, - { - choices: [ - { - delta: { - tool_calls: [ - { - function: { arguments: "2}" }, - }, - ], - }, - }, - ], - }, - ]; - - const result = aggregateChatCompletionChunks(chunks as any); - const output = result.output as any[]; - - expect(output[0].message.tool_calls).toHaveLength(2); - expect(output[0].message.tool_calls[0]).toEqual({ - id: "call_1", - type: "function", - function: { name: "tool1", arguments: '{"a":1}' }, - }); - expect(output[0].message.tool_calls[1]).toEqual({ - id: "call_2", - type: "function", - function: { name: "tool2", arguments: '{"b":2}' }, - }); - }); - - it("should parse usage metrics from chunks", () => { - const chunks = [ - { - choices: [{ delta: { role: "assistant", content: "Hi" } }], - }, - { - choices: [{ delta: { content: "!" } }], - usage: { - prompt_tokens: 10, - completion_tokens: 2, - total_tokens: 12, - }, - }, - ]; - - const result = aggregateChatCompletionChunks(chunks as any); - - expect(result.metrics).toEqual({ - prompt_tokens: 10, - completion_tokens: 2, - tokens: 12, // total_tokens is mapped to tokens - }); - }); - - it("should merge usage from multiple chunks", () => { - const chunks = [ - { - choices: [{ delta: { role: "assistant" } }], - usage: { - prompt_tokens: 10, - }, - }, - { - choices: [{ delta: { content: "Hi" } }], - usage: { - completion_tokens: 5, - total_tokens: 15, - }, - }, - ]; - - const result = aggregateChatCompletionChunks(chunks); - - expect(result.metrics).toEqual({ - prompt_tokens: 10, - completion_tokens: 5, - tokens: 15, // total_tokens is mapped to tokens - }); - }); - - it("should handle empty chunks array", () => { - const result = aggregateChatCompletionChunks([]); - - expect(result.output).toEqual([ - { - index: 0, - message: { - role: undefined, - content: undefined, - tool_calls: undefined, - }, - logprobs: null, - finish_reason: undefined, - }, - ]); - expect(result.metrics).toEqual({}); - }); - - it("should handle chunks without choices", () => { - const chunks = [ - {}, - { choices: null }, - { choices: [] }, - { choices: [{ delta: { content: "Hi" } }] }, - ]; - - const result = aggregateChatCompletionChunks(chunks as any); - - expect(result.output[0].message.content).toBe("Hi"); - }); - - it("should handle chunks with only usage", () => { - const chunks = [ - { - usage: { - prompt_tokens: 10, - completion_tokens: 5, - }, - }, - ]; - - const result = aggregateChatCompletionChunks(chunks); - - expect(result.metrics).toEqual({ - prompt_tokens: 10, - completion_tokens: 5, - }); - }); - - it("should handle mixed content and tool calls", () => { - const chunks = [ - { - choices: [ - { - delta: { - role: "assistant", - content: "Let me check", - }, - }, - ], - }, - { - choices: [ - { - delta: { - tool_calls: [ - { - id: "call_1", - type: "function", - function: { name: "check", arguments: "{}" }, - }, - ], - }, - }, - ], - }, - { - choices: [{ delta: { finish_reason: "tool_calls" } }], - }, - ]; - - const result = aggregateChatCompletionChunks(chunks); - - expect(result.output[0].message.content).toBe("Let me check"); - expect(result.output[0].message.tool_calls).toHaveLength(1); - expect(result.output[0].finish_reason).toBe("tool_calls"); - }); -}); diff --git a/js/src/instrumentation/braintrust-plugin.ts b/js/src/instrumentation/braintrust-plugin.ts deleted file mode 100644 index 53bebc00d..000000000 --- a/js/src/instrumentation/braintrust-plugin.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { BasePlugin } from "./core"; -import { OpenAIPlugin } from "./plugins/openai-plugin"; -import { OpenAICodexPlugin } from "./plugins/openai-codex-plugin"; -import { AnthropicPlugin } from "./plugins/anthropic-plugin"; -import { AISDKPlugin } from "./plugins/ai-sdk-plugin"; -import { ClaudeAgentSDKPlugin } from "./plugins/claude-agent-sdk-plugin"; -import { CloudflareThinkPlugin } from "./plugins/cloudflare-think-plugin"; -import { CursorSDKPlugin } from "./plugins/cursor-sdk-plugin"; -import { OpenAIAgentsPlugin } from "./plugins/openai-agents-plugin"; -import { GoogleGenAIPlugin } from "./plugins/google-genai-plugin"; -import { HuggingFacePlugin } from "./plugins/huggingface-plugin"; -import { HuggingFaceTransformersPlugin } from "./plugins/huggingface-transformers-plugin"; -import { OpenRouterAgentPlugin } from "./plugins/openrouter-agent-plugin"; -import { OpenRouterPlugin } from "./plugins/openrouter-plugin"; -import { MistralPlugin } from "./plugins/mistral-plugin"; -import { OllamaPlugin } from "./plugins/ollama-plugin"; -import { GoogleADKPlugin } from "./plugins/google-adk-plugin"; -import { CoherePlugin } from "./plugins/cohere-plugin"; -import { GroqPlugin } from "./plugins/groq-plugin"; -import { BedrockRuntimePlugin } from "./plugins/bedrock-runtime-plugin"; -import { GenkitPlugin } from "./plugins/genkit-plugin"; -import { GitHubCopilotPlugin } from "./plugins/github-copilot-plugin"; -import { LangChainPlugin } from "./plugins/langchain-plugin"; -import { LangSmithPlugin } from "./plugins/langsmith-plugin"; -import { PiCodingAgentPlugin } from "./plugins/pi-coding-agent-plugin"; -import { StrandsAgentSDKPlugin } from "./plugins/strands-agent-sdk-plugin"; -import { VoyageAIPlugin } from "./plugins/voyageai-plugin"; -import { CloudflareAIChatPlugin } from "./plugins/cloudflare-ai-chat-plugin"; -import { CloudflareAgentsPlugin } from "./plugins/cloudflare-agents-plugin"; -import type { InstrumentationIntegrationsConfig } from "./config"; - -export interface BraintrustPluginConfig { - integrations?: InstrumentationIntegrationsConfig; -} - -/** - * Default Braintrust plugin that manages all AI provider instrumentation plugins. - * - * This plugin orchestrates: - * - OpenAI SDK (chat completions, embeddings, etc.) - * - Anthropic SDK (messages) - * - Claude Agent SDK (agent interactions) - * - Vercel AI SDK (generateText, streamText, etc.) - * - Google GenAI SDK - * - HuggingFace Inference SDK - * - LangChain.js and LangGraph - * - Mistral SDK - * - Ollama SDK - * - Cohere SDK - * - Voyage AI SDK - * - * The plugin is automatically enabled when the Braintrust library is loaded. - * Individual integrations can be disabled via configuration. - */ -export class BraintrustPlugin extends BasePlugin { - private config: BraintrustPluginConfig; - private openaiPlugin: OpenAIPlugin | null = null; - private openAICodexPlugin: OpenAICodexPlugin | null = null; - private anthropicPlugin: AnthropicPlugin | null = null; - private aiSDKPlugin: AISDKPlugin | null = null; - private claudeAgentSDKPlugin: ClaudeAgentSDKPlugin | null = null; - private cloudflareThinkPlugin: CloudflareThinkPlugin | null = null; - private cursorSDKPlugin: CursorSDKPlugin | null = null; - private openAIAgentsPlugin: OpenAIAgentsPlugin | null = null; - private googleGenAIPlugin: GoogleGenAIPlugin | null = null; - private huggingFacePlugin: HuggingFacePlugin | null = null; - private huggingFaceTransformersPlugin: HuggingFaceTransformersPlugin | null = - null; - private openRouterPlugin: OpenRouterPlugin | null = null; - private openRouterAgentPlugin: OpenRouterAgentPlugin | null = null; - private mistralPlugin: MistralPlugin | null = null; - private ollamaPlugin: OllamaPlugin | null = null; - private googleADKPlugin: GoogleADKPlugin | null = null; - private coherePlugin: CoherePlugin | null = null; - private groqPlugin: GroqPlugin | null = null; - private bedrockRuntimePlugin: BedrockRuntimePlugin | null = null; - private genkitPlugin: GenkitPlugin | null = null; - private gitHubCopilotPlugin: GitHubCopilotPlugin | null = null; - private langChainPlugin: LangChainPlugin | null = null; - private langSmithPlugin: LangSmithPlugin | null = null; - private piCodingAgentPlugin: PiCodingAgentPlugin | null = null; - private strandsAgentSDKPlugin: StrandsAgentSDKPlugin | null = null; - private voyageAIPlugin: VoyageAIPlugin | null = null; - private cloudflareAIChatPlugin: CloudflareAIChatPlugin | null = null; - private cloudflareAgentsPlugin: CloudflareAgentsPlugin | null = null; - - constructor(config: BraintrustPluginConfig = {}) { - super(); - this.config = config; - } - - protected onEnable(): void { - const integrations = this.config.integrations ?? {}; - - // Enable OpenAI integration (default: true) - if (integrations.openai !== false) { - this.openaiPlugin = new OpenAIPlugin(); - this.openaiPlugin.enable(); - } - - if (integrations.openaiCodexSDK !== false) { - this.openAICodexPlugin = new OpenAICodexPlugin(); - this.openAICodexPlugin.enable(); - } - - // Enable Anthropic integration (default: true) - if (integrations.anthropic !== false) { - this.anthropicPlugin = new AnthropicPlugin(); - this.anthropicPlugin.enable(); - } - - // Enable AI SDK integration (default: true) - // Support both 'aisdk' and legacy 'vercel' config keys - if (integrations.aisdk !== false && integrations.vercel !== false) { - this.aiSDKPlugin = new AISDKPlugin(); - this.aiSDKPlugin.enable(); - } - - // Enable Claude Agent SDK integration (default: true) - if (integrations.claudeAgentSDK !== false) { - this.claudeAgentSDKPlugin = new ClaudeAgentSDKPlugin(); - this.claudeAgentSDKPlugin.enable(); - } - - if (integrations.cloudflareThink !== false) { - this.cloudflareThinkPlugin = new CloudflareThinkPlugin(); - this.cloudflareThinkPlugin.enable(); - } - - if (integrations.cursorSDK !== false && integrations.cursor !== false) { - this.cursorSDKPlugin = new CursorSDKPlugin(); - this.cursorSDKPlugin.enable(); - } - - // Enable OpenAI Agents SDK integration (default: true) - if (integrations.openAIAgents !== false) { - this.openAIAgentsPlugin = new OpenAIAgentsPlugin(); - this.openAIAgentsPlugin.enable(); - } - - // Enable Google GenAI integration (default: true) - // Support both 'googleGenAI' and legacy 'google' config keys - if (integrations.googleGenAI !== false && integrations.google !== false) { - this.googleGenAIPlugin = new GoogleGenAIPlugin(); - this.googleGenAIPlugin.enable(); - } - - if (integrations.huggingface !== false) { - this.huggingFacePlugin = new HuggingFacePlugin(); - this.huggingFacePlugin.enable(); - this.huggingFaceTransformersPlugin = new HuggingFaceTransformersPlugin(); - this.huggingFaceTransformersPlugin.enable(); - } - - if (integrations.openrouter !== false) { - this.openRouterPlugin = new OpenRouterPlugin(); - this.openRouterPlugin.enable(); - } - - if (integrations.openrouterAgent !== false) { - this.openRouterAgentPlugin = new OpenRouterAgentPlugin(); - this.openRouterAgentPlugin.enable(); - } - - if (integrations.mistral !== false) { - this.mistralPlugin = new MistralPlugin(); - this.mistralPlugin.enable(); - } - - if (integrations.ollama !== false) { - this.ollamaPlugin = new OllamaPlugin(); - this.ollamaPlugin.enable(); - } - - // Enable Google ADK integration (default: true) - if (integrations.googleADK !== false) { - this.googleADKPlugin = new GoogleADKPlugin(); - this.googleADKPlugin.enable(); - } - - if (integrations.cohere !== false) { - this.coherePlugin = new CoherePlugin(); - this.coherePlugin.enable(); - } - - if (integrations.voyageai !== false) { - this.voyageAIPlugin = new VoyageAIPlugin(); - this.voyageAIPlugin.enable(); - } - - if (integrations.groq !== false) { - this.groqPlugin = new GroqPlugin(); - this.groqPlugin.enable(); - } - - if ( - integrations.bedrock !== false && - integrations.awsBedrock !== false && - integrations.awsBedrockRuntime !== false - ) { - this.bedrockRuntimePlugin = new BedrockRuntimePlugin(); - this.bedrockRuntimePlugin.enable(); - } - - if (integrations.genkit !== false) { - this.genkitPlugin = new GenkitPlugin(); - this.genkitPlugin.enable(); - } - - if (integrations.gitHubCopilot !== false) { - this.gitHubCopilotPlugin = new GitHubCopilotPlugin(); - this.gitHubCopilotPlugin.enable(); - } - - if (integrations.piCodingAgent !== false) { - this.piCodingAgentPlugin = new PiCodingAgentPlugin(); - this.piCodingAgentPlugin.enable(); - } - - if (integrations.strandsAgentSDK !== false) { - this.strandsAgentSDKPlugin = new StrandsAgentSDKPlugin(); - this.strandsAgentSDKPlugin.enable(); - } - - if (integrations.cloudflareAIChat !== false) { - this.cloudflareAIChatPlugin = new CloudflareAIChatPlugin(); - this.cloudflareAIChatPlugin.enable(); - } - - if (integrations.cloudflareAgents !== false) { - this.cloudflareAgentsPlugin = new CloudflareAgentsPlugin(); - this.cloudflareAgentsPlugin.enable(); - } - - if (integrations.langchain !== false && integrations.langgraph !== false) { - this.langChainPlugin = new LangChainPlugin(); - this.langChainPlugin.enable(); - } - - if (integrations.langsmith !== false) { - this.langSmithPlugin = new LangSmithPlugin({ - skipLangChainRuns: integrations.langchain !== false, - }); - this.langSmithPlugin.enable(); - } - - // Mastra is intentionally not wired here: `@mastra/core` ships its own - // ObservabilityExporter contract, and `BraintrustObservabilityExporter` - // (wrappers/mastra.ts) is auto-installed by the loader patch in - // `auto-instrumentations/loader/mastra-observability-patch.ts` rather than - // by a BasePlugin / global hook subscription. - } - - protected onDisable(): void { - if (this.openaiPlugin) { - this.openaiPlugin.disable(); - this.openaiPlugin = null; - } - - if (this.openAICodexPlugin) { - this.openAICodexPlugin.disable(); - this.openAICodexPlugin = null; - } - - if (this.anthropicPlugin) { - this.anthropicPlugin.disable(); - this.anthropicPlugin = null; - } - - if (this.aiSDKPlugin) { - this.aiSDKPlugin.disable(); - this.aiSDKPlugin = null; - } - - if (this.claudeAgentSDKPlugin) { - this.claudeAgentSDKPlugin.disable(); - this.claudeAgentSDKPlugin = null; - } - - if (this.cloudflareThinkPlugin) { - this.cloudflareThinkPlugin.disable(); - this.cloudflareThinkPlugin = null; - } - - if (this.cursorSDKPlugin) { - this.cursorSDKPlugin.disable(); - this.cursorSDKPlugin = null; - } - - if (this.openAIAgentsPlugin) { - this.openAIAgentsPlugin.disable(); - this.openAIAgentsPlugin = null; - } - - if (this.googleGenAIPlugin) { - this.googleGenAIPlugin.disable(); - this.googleGenAIPlugin = null; - } - - if (this.huggingFacePlugin) { - this.huggingFacePlugin.disable(); - this.huggingFacePlugin = null; - } - - if (this.huggingFaceTransformersPlugin) { - this.huggingFaceTransformersPlugin.disable(); - this.huggingFaceTransformersPlugin = null; - } - - if (this.openRouterPlugin) { - this.openRouterPlugin.disable(); - this.openRouterPlugin = null; - } - - if (this.openRouterAgentPlugin) { - this.openRouterAgentPlugin.disable(); - this.openRouterAgentPlugin = null; - } - - if (this.mistralPlugin) { - this.mistralPlugin.disable(); - this.mistralPlugin = null; - } - - if (this.ollamaPlugin) { - this.ollamaPlugin.disable(); - this.ollamaPlugin = null; - } - - if (this.googleADKPlugin) { - this.googleADKPlugin.disable(); - this.googleADKPlugin = null; - } - - if (this.coherePlugin) { - this.coherePlugin.disable(); - this.coherePlugin = null; - } - - if (this.voyageAIPlugin) { - this.voyageAIPlugin.disable(); - this.voyageAIPlugin = null; - } - - if (this.groqPlugin) { - this.groqPlugin.disable(); - this.groqPlugin = null; - } - - if (this.bedrockRuntimePlugin) { - this.bedrockRuntimePlugin.disable(); - this.bedrockRuntimePlugin = null; - } - - if (this.genkitPlugin) { - this.genkitPlugin.disable(); - this.genkitPlugin = null; - } - - if (this.gitHubCopilotPlugin) { - this.gitHubCopilotPlugin.disable(); - this.gitHubCopilotPlugin = null; - } - - if (this.piCodingAgentPlugin) { - this.piCodingAgentPlugin.disable(); - this.piCodingAgentPlugin = null; - } - - if (this.strandsAgentSDKPlugin) { - this.strandsAgentSDKPlugin.disable(); - this.strandsAgentSDKPlugin = null; - } - - if (this.cloudflareAIChatPlugin) { - this.cloudflareAIChatPlugin.disable(); - this.cloudflareAIChatPlugin = null; - } - - if (this.cloudflareAgentsPlugin) { - this.cloudflareAgentsPlugin.disable(); - this.cloudflareAgentsPlugin = null; - } - - if (this.langChainPlugin) { - this.langChainPlugin.disable(); - this.langChainPlugin = null; - } - - if (this.langSmithPlugin) { - this.langSmithPlugin.disable(); - this.langSmithPlugin = null; - } - } -} - -// Re-export utility functions from OpenAIPlugin for backward compatibility -export { - parseMetricsFromUsage, - processImagesInOutput, - aggregateChatCompletionChunks, -} from "./plugins/openai-plugin"; diff --git a/js/src/instrumentation/core/channel-tracing.test.ts b/js/src/instrumentation/core/channel-tracing.test.ts index 55c08f05b..319d24344 100644 --- a/js/src/instrumentation/core/channel-tracing.test.ts +++ b/js/src/instrumentation/core/channel-tracing.test.ts @@ -27,12 +27,32 @@ import { traceAsyncChannel, traceStreamingChannel } from "./channel-tracing"; const testChannels = defineChannels( "channel-tracing-test", { - asyncCall: channel<[Record], { ok: true }>({ - channelName: "async.call", + asyncBinding: channel<[Record], { ok: true }>({ + channelName: "async.binding", kind: "async", }), - streamingCall: channel<[Record], { ok: true }>({ - channelName: "streaming.call", + provenance: channel<[Record], { ok: true }>({ + channelName: "async.provenance", + kind: "async", + }), + skipped: channel<[Record], { ok: true }>({ + channelName: "async.skipped", + kind: "async", + }), + throwingPredicate: channel<[Record], { ok: true }>({ + channelName: "async.throwing-predicate", + kind: "async", + }), + suppressed: channel<[Record], { ok: true }>({ + channelName: "async.suppressed", + kind: "async", + }), + streamingCleanup: channel<[Record], { ok: true }>({ + channelName: "streaming.cleanup", + kind: "async", + }), + streamingCancellation: channel<[Record], { ok: true }>({ + channelName: "streaming.cancellation", kind: "async", }), }, @@ -60,7 +80,7 @@ describe("traceAsyncChannel current span binding", () => { }); it("binds the created span into the traced async execution context", async () => { - const unsubscribe = traceAsyncChannel(testChannels.asyncCall, { + traceAsyncChannel(testChannels.asyncBinding, { name: "channel-tracing-test", type: "function", extractInput: () => ({ @@ -73,21 +93,16 @@ describe("traceAsyncChannel current span binding", () => { const seenSpanIds: string[] = []; - try { - await testChannels.asyncCall.tracePromise( - async () => { - seenSpanIds.push(currentSpan().spanId); - await Promise.resolve(); - seenSpanIds.push(currentSpan().spanId); - - return { ok: true as const }; - }, - { arguments: [{}] } as any, - ); - } finally { - unsubscribe(); - } + await testChannels.asyncBinding.tracePromise( + async () => { + seenSpanIds.push(currentSpan().spanId); + await Promise.resolve(); + seenSpanIds.push(currentSpan().spanId); + return { ok: true as const }; + }, + { arguments: [{}] } as any, + ); expect(seenSpanIds).toHaveLength(2); expect(seenSpanIds[0]).toBeTruthy(); expect(seenSpanIds[1]).toBe(seenSpanIds[0]); @@ -103,7 +118,7 @@ describe("traceAsyncChannel current span binding", () => { }); it("limits channel provenance to directly instrumented spans", async () => { - const unsubscribe = traceAsyncChannel(testChannels.asyncCall, { + traceAsyncChannel(testChannels.provenance, { name: "channel-parent", type: "function", extractInput: () => ({ input: "input", metadata: undefined }), @@ -111,32 +126,27 @@ describe("traceAsyncChannel current span binding", () => { extractMetrics: () => ({}), }); - try { - await testChannels.asyncCall.tracePromise( - async () => { - const parent = currentSpan(); - parent.startSpan({ name: "user-child" }).end(); - parent - .startSpanWithParents("user-multi-parent-child", [parent.spanId], { - name: "user-multi-parent-child", - }) - .end(); - parent - .startSpan( - withSpanInstrumentationName( - { name: "instrumentation-child" }, - INSTRUMENTATION_NAMES.OPENAI, - ), - ) - .end(); - return { ok: true as const }; - }, - { arguments: [{}] } as any, - ); - } finally { - unsubscribe(); - } - + await testChannels.provenance.tracePromise( + async () => { + const parent = currentSpan(); + parent.startSpan({ name: "user-child" }).end(); + parent + .startSpanWithParents("user-multi-parent-child", [parent.spanId], { + name: "user-multi-parent-child", + }) + .end(); + parent + .startSpan( + withSpanInstrumentationName( + { name: "instrumentation-child" }, + INSTRUMENTATION_NAMES.OPENAI, + ), + ) + .end(); + return { ok: true as const }; + }, + { arguments: [{}] } as any, + ); const spans = (await backgroundLogger.drain()) as Array< Record >; @@ -161,7 +171,7 @@ describe("traceAsyncChannel current span binding", () => { }); it("does not create a span when shouldTrace returns false", async () => { - const unsubscribe = traceAsyncChannel(testChannels.asyncCall, { + traceAsyncChannel(testChannels.skipped, { name: "channel-tracing-test", shouldTrace: ([params]) => !( @@ -181,21 +191,16 @@ describe("traceAsyncChannel current span binding", () => { const seenSpanIds: string[] = []; - try { - await testChannels.asyncCall.tracePromise( - async () => { - seenSpanIds.push(currentSpan().spanId); - await Promise.resolve(); - seenSpanIds.push(currentSpan().spanId); - - return { ok: true as const }; - }, - { arguments: [{ skip: true }] } as any, - ); - } finally { - unsubscribe(); - } + await testChannels.skipped.tracePromise( + async () => { + seenSpanIds.push(currentSpan().spanId); + await Promise.resolve(); + seenSpanIds.push(currentSpan().spanId); + return { ok: true as const }; + }, + { arguments: [{ skip: true }] } as any, + ); expect(seenSpanIds).toEqual(["", ""]); expect(currentSpan()).toBe(NOOP_SPAN); @@ -207,7 +212,7 @@ describe("traceAsyncChannel current span binding", () => { const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementation(() => {}); - const unsubscribe = traceAsyncChannel(testChannels.asyncCall, { + traceAsyncChannel(testChannels.throwingPredicate, { name: "channel-tracing-test", shouldTrace: () => { throw new Error("predicate failed"); @@ -221,15 +226,10 @@ describe("traceAsyncChannel current span binding", () => { extractMetrics: () => ({}), }); - try { - await testChannels.asyncCall.tracePromise( - async () => ({ ok: true as const }), - { arguments: [{}] } as any, - ); - } finally { - unsubscribe(); - } - + await testChannels.throwingPredicate.tracePromise( + async () => ({ ok: true as const }), + { arguments: [{}] } as any, + ); expect(consoleErrorSpy).not.toHaveBeenCalled(); consoleErrorSpy.mockRestore(); @@ -238,7 +238,7 @@ describe("traceAsyncChannel current span binding", () => { }); it("skips auto instrumentation spans while suppression is active", async () => { - const unsubscribe = traceAsyncChannel(testChannels.asyncCall, { + traceAsyncChannel(testChannels.suppressed, { name: "channel-tracing-test", type: "function", extractInput: () => ({ @@ -249,23 +249,18 @@ describe("traceAsyncChannel current span binding", () => { extractMetrics: () => ({}), }); - try { - await runWithAutoInstrumentationSuppressed(() => - testChannels.asyncCall.tracePromise( - async () => { - expect(currentSpan()).toBe(NOOP_SPAN); - await Promise.resolve(); - expect(currentSpan()).toBe(NOOP_SPAN); - - return { ok: true as const }; - }, - { arguments: [{}] } as any, - ), - ); - } finally { - unsubscribe(); - } + await runWithAutoInstrumentationSuppressed(() => + testChannels.suppressed.tracePromise( + async () => { + expect(currentSpan()).toBe(NOOP_SPAN); + await Promise.resolve(); + expect(currentSpan()).toBe(NOOP_SPAN); + return { ok: true as const }; + }, + { arguments: [{}] } as any, + ), + ); const spans = await backgroundLogger.drain(); expect(spans).toHaveLength(0); }); @@ -280,7 +275,7 @@ describe("traceAsyncChannel current span binding", () => { throw new Error("logging failed"); }), } as unknown as Span; - const unsubscribe = traceStreamingChannel(testChannels.streamingCall, { + traceStreamingChannel(testChannels.streamingCleanup, { name: "streaming-channel-test", startSpan: () => child, type: "function", @@ -291,25 +286,20 @@ describe("traceAsyncChannel current span binding", () => { onError, }); - try { - await expect( - testChannels.streamingCall.tracePromise( - async () => ({ ok: true as const }), - { arguments: [{}] } as any, - ), - ).resolves.toEqual({ ok: true }); - await expect( - testChannels.streamingCall.tracePromise( - async () => { - throw new Error("call failed"); - }, - { arguments: [{}] } as any, - ), - ).rejects.toThrow("call failed"); - } finally { - unsubscribe(); - } - + await expect( + testChannels.streamingCleanup.tracePromise( + async () => ({ ok: true as const }), + { arguments: [{}] } as any, + ), + ).resolves.toEqual({ ok: true }); + await expect( + testChannels.streamingCleanup.tracePromise( + async () => { + throw new Error("call failed"); + }, + { arguments: [{}] } as any, + ), + ).rejects.toThrow("call failed"); expect(onComplete).toHaveBeenCalledTimes(1); expect(onError).toHaveBeenCalledTimes(1); expect(end).toHaveBeenCalledTimes(2); @@ -321,7 +311,7 @@ describe("traceAsyncChannel current span binding", () => { end: vi.fn(), log: vi.fn(), } as unknown as Span; - const unsubscribe = traceStreamingChannel(testChannels.streamingCall, { + traceStreamingChannel(testChannels.streamingCancellation, { name: "streaming-channel-test", startSpan: () => child, type: "function", @@ -337,17 +327,12 @@ describe("traceAsyncChannel current span binding", () => { }, }; - try { - const patched = await testChannels.streamingCall.tracePromise( - async () => stream as any, - { arguments: [{}] } as any, - ); - (patched as unknown as typeof stream).abort(); - await Promise.resolve(); - } finally { - unsubscribe(); - } - + const patched = await testChannels.streamingCancellation.tracePromise( + async () => stream as any, + { arguments: [{}] } as any, + ); + (patched as unknown as typeof stream).abort(); + await Promise.resolve(); const cancellationError = expect.objectContaining({ message: "Stream cancelled before completion", name: "AbortError", diff --git a/js/src/instrumentation/core/channel-tracing.ts b/js/src/instrumentation/core/channel-tracing.ts index a8ecc819c..1f3ff47d0 100644 --- a/js/src/instrumentation/core/channel-tracing.ts +++ b/js/src/instrumentation/core/channel-tracing.ts @@ -312,7 +312,7 @@ function bindCurrentSpanStoreToStart< }, channelName: string, instrumentationName: SpanInstrumentationName, -): (() => void) | undefined { +): void { const state = _internalGetGlobalState(); const startChannel = tracingChannel.start; const contextManager = state?.contextManager; @@ -325,7 +325,7 @@ function bindCurrentSpanStoreToStart< : undefined; if (!currentSpanStore || !startChannel) { - return undefined; + return; } startChannel.bindStore( @@ -347,10 +347,6 @@ function bindCurrentSpanStoreToStart< : currentSpanStore.getStore(); }, ); - - return () => { - startChannel.unbindStore(currentSpanStore); - }; } function logErrorAndEnd< @@ -442,13 +438,13 @@ function runStreamingErrorHook(args: { export function traceAsyncChannel( channel: TChannel, config: AsyncChannelSpanConfig, -): () => void { +): void { const tracingChannel = channel.tracingChannel() as IsoTracingChannel< ChannelMessage >; const states = new WeakMap(); const channelName = channel.channelName; - const unbindCurrentSpanStore = bindCurrentSpanStoreToStart( + bindCurrentSpanStoreToStart( tracingChannel, states, config, @@ -514,23 +510,18 @@ export function traceAsyncChannel( }; tracingChannel.subscribe(handlers); - - return () => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }; } export function traceStreamingChannel( channel: TChannel, config: StreamingChannelSpanConfig, -): () => void { +): void { const tracingChannel = channel.tracingChannel() as IsoTracingChannel< ChannelMessage >; const states = new WeakMap(); const channelName = channel.channelName; - const unbindCurrentSpanStore = bindCurrentSpanStoreToStart( + bindCurrentSpanStoreToStart( tracingChannel, states, config, @@ -790,23 +781,18 @@ export function traceStreamingChannel( }; tracingChannel.subscribe(handlers); - - return () => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }; } export function traceSyncStreamChannel( channel: TChannel, config: SyncStreamChannelSpanConfig, -): () => void { +): void { const tracingChannel = channel.tracingChannel() as IsoTracingChannel< ChannelMessage >; const states = new WeakMap(); const channelName = channel.channelName; - const unbindCurrentSpanStore = bindCurrentSpanStoreToStart( + bindCurrentSpanStoreToStart( tracingChannel, states, config, @@ -939,19 +925,4 @@ export function traceSyncStreamChannel( }; tracingChannel.subscribe(handlers); - - return () => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }; -} - -export function unsubscribeAll( - unsubscribers: Array<() => void>, -): Array<() => void> { - for (const unsubscribe of unsubscribers) { - unsubscribe(); - } - - return []; } diff --git a/js/src/instrumentation/core/channel.ts b/js/src/instrumentation/core/channel.ts deleted file mode 100644 index 8cbf253d6..000000000 --- a/js/src/instrumentation/core/channel.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Utilities for instrumentation hook naming and management. - */ - -/** - * Channel naming convention: braintrust:{component}:{operation} - * - * Examples: - * - braintrust:openai:chat.completions.create - * - braintrust:anthropic:messages.create - * - braintrust:ai-sdk:generateText - */ - -/** - * Creates a standardized channel name. - * - * @param component - The SDK/library being instrumented (e.g., 'openai', 'anthropic') - * @param operation - The operation being traced (e.g., 'chat.completions.create') - * @returns The full channel name - */ -export function createChannelName( - component: string, - operation: string, -): string { - return `braintrust:${component}:${operation}`; -} - -/** - * Parses a channel name into its component parts. - * - * @param channelName - The full channel name - * @returns Object with component and operation, or null if invalid - */ -export function parseChannelName( - channelName: string, -): { component: string; operation: string } | null { - const match = channelName.match(/^braintrust:([^:]+):(.+)$/); - if (!match) { - return null; - } - return { - component: match[1], - operation: match[2], - }; -} - -/** - * Validates a channel name follows the expected convention. - * - * @param channelName - The channel name to validate - * @returns True if valid - */ -export function isValidChannelName(channelName: string): boolean { - return /^braintrust:[^:]+:.+$/.test(channelName); -} diff --git a/js/src/instrumentation/core/index.ts b/js/src/instrumentation/core/index.ts index 6e2d2d61c..edfe17a89 100644 --- a/js/src/instrumentation/core/index.ts +++ b/js/src/instrumentation/core/index.ts @@ -1,27 +1,3 @@ -/** - * Core utilities for building auto-instrumentation plugins. - * - * Provides BasePlugin class and channel utilities following the OpenTelemetry - * InstrumentationBase pattern - core infrastructure lives here, but individual - * instrumentations can be separate packages. - * - * Note: auto-instrumentation config types are exposed from the Braintrust - * bundler subpaths, such as `braintrust/vite`. - */ +/** Internal utilities shared by Braintrust instrumentation consumers. */ -export { BasePlugin } from "./plugin"; export { toLoggedError } from "./logging"; -export { - createChannelName, - parseChannelName, - isValidChannelName, -} from "./channel"; -export type { - BaseContext, - StartEvent, - EndEvent, - ErrorEvent, - AsyncStartEvent, - AsyncEndEvent, - ChannelHandlers, -} from "./types"; diff --git a/js/src/instrumentation/core/plugin.ts b/js/src/instrumentation/core/plugin.ts deleted file mode 100644 index ae09a43f1..000000000 --- a/js/src/instrumentation/core/plugin.ts +++ /dev/null @@ -1,505 +0,0 @@ -import iso from "../../isomorph"; -import type { IsoChannelHandlers } from "../../isomorph"; -import { isAsyncIterable, patchStreamIfNeeded } from "./stream-patcher"; -import type { StartEvent } from "./types"; -import { startSpan } from "../../logger"; -import type { Span } from "../../logger"; -import { getCurrentUnixTimestamp } from "../../util"; -import { - buildStartSpanArgs, - mergeInputMetadata, -} from "./channel-tracing-utils"; - -/** - * Base class for creating instrumentation plugins. - * - * Plugins subscribe to global instrumentation hook events and convert them - * into spans, logs, or other observability data. - */ -export abstract class BasePlugin { - protected enabled = false; - protected unsubscribers: Array<() => void> = []; - - /** - * Enables the plugin. Must be called before the plugin will receive events. - */ - enable(): void { - if (this.enabled) { - return; - } - this.enabled = true; - this.onEnable(); - } - - /** - * Disables the plugin. After this, the plugin will no longer receive events. - */ - disable(): void { - if (!this.enabled) { - return; - } - this.enabled = false; - this.onDisable(); - } - - /** - * Called when the plugin is enabled. - * Override this to set up subscriptions. - */ - protected abstract onEnable(): void; - - /** - * Called when the plugin is disabled. - * Override this to clean up subscriptions. - */ - protected abstract onDisable(): void; - - /** - * Helper to subscribe to a channel with raw handlers. - * - * @param channelName - The channel name to subscribe to - * @param handlers - Event handlers - */ - protected subscribe(channelName: string, handlers: IsoChannelHandlers): void { - const channel = iso.newTracingChannel(channelName); - channel.subscribe(handlers); - } - - /** - * Subscribe to a channel for async methods (non-streaming). - * Creates a span and logs input/output/metrics. - */ - protected subscribeToChannel( - channelName: string, - config: { - name: string; - type: string; - extractInput: (args: any[]) => { input: any; metadata: any }; - extractOutput: (result: any, endEvent?: any) => any; - extractMetadata?: (result: any, endEvent?: any) => any; - extractMetrics: ( - result: any, - startTime?: number, - endEvent?: any, - ) => Record; - }, - ): void { - const channel = iso.newTracingChannel(channelName); - - const spans = new WeakMap(); - - const handlers = { - start: (event: StartEvent) => { - const { name, spanAttributes, spanInfoMetadata } = buildStartSpanArgs( - config, - event, - ); - const span = startSpan({ - name, - spanAttributes, - }); - - const startTime = getCurrentUnixTimestamp(); - spans.set(event, { span, startTime }); - - try { - const { input, metadata } = config.extractInput(event.arguments); - span.log({ - input, - metadata: mergeInputMetadata(metadata, spanInfoMetadata), - }); - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(`Error extracting input for ${channelName}:`, error); - } - }, - - asyncEnd: (event: any) => { - const spanData = spans.get(event); - if (!spanData) { - return; - } - - const { span, startTime } = spanData; - - try { - const output = config.extractOutput(event.result, event); - const metrics = config.extractMetrics(event.result, startTime, event); - const metadata = config.extractMetadata?.(event.result, event); - - span.log({ - output, - ...(metadata !== undefined ? { metadata } : {}), - metrics, - }); - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(`Error extracting output for ${channelName}:`, error); - } finally { - span.end(); - spans.delete(event); - } - }, - - error: (event: any) => { - const spanData = spans.get(event); - if (!spanData) { - return; - } - - const { span } = spanData; - - span.log({ - error: event.error.message, - }); - span.end(); - spans.delete(event); - }, - }; - - channel.subscribe(handlers); - - // Store unsubscribe function - this.unsubscribers.push(() => { - channel.unsubscribe(handlers); - }); - } - - /** - * Subscribe to a channel for async methods that may return streams. - * Handles both streaming and non-streaming responses. - */ - protected subscribeToStreamingChannel( - channelName: string, - config: { - name: string; - type: string; - extractInput: (args: any[]) => { input: any; metadata: any }; - extractOutput: (result: any, endEvent?: any) => any; - extractMetadata?: (result: any, endEvent?: any) => any; - extractMetrics: ( - result: any, - startTime?: number, - endEvent?: any, - ) => Record; - aggregateChunks?: ( - chunks: any[], - result?: any, - endEvent?: any, - ) => { - output: any; - metrics: Record; - metadata?: any; - }; - }, - ): void { - const channel = iso.newTracingChannel(channelName); - - const spans = new WeakMap(); - - const handlers = { - start: (event: StartEvent) => { - const { name, spanAttributes, spanInfoMetadata } = buildStartSpanArgs( - config, - event, - ); - const span = startSpan({ - name, - spanAttributes, - }); - - const startTime = getCurrentUnixTimestamp(); - spans.set(event, { span, startTime }); - - try { - const { input, metadata } = config.extractInput(event.arguments); - span.log({ - input, - metadata: mergeInputMetadata(metadata, spanInfoMetadata), - }); - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(`Error extracting input for ${channelName}:`, error); - } - }, - - asyncEnd: (event: any) => { - const spanData = spans.get(event); - if (!spanData) { - return; - } - - const { span, startTime } = spanData; - - // Check if result is a stream - if (isAsyncIterable(event.result)) { - let firstChunkTime: number | undefined; - - // Patch the stream to collect chunks - patchStreamIfNeeded(event.result, { - onChunk: () => { - if (firstChunkTime === undefined) { - firstChunkTime = getCurrentUnixTimestamp(); - } - }, - onComplete: (chunks: any[]) => { - try { - let output: any; - let metrics: Record; - let metadata: any; - - if (config.aggregateChunks) { - const aggregated = config.aggregateChunks( - chunks, - event.result, - event, - ); - output = aggregated.output; - metrics = aggregated.metrics; - metadata = aggregated.metadata; - } else { - output = config.extractOutput(chunks, event); - metrics = config.extractMetrics(chunks, startTime, event); - } - - // Add time_to_first_token if not already present - if ( - metrics.time_to_first_token === undefined && - firstChunkTime !== undefined - ) { - metrics.time_to_first_token = firstChunkTime - startTime; - } else if ( - metrics.time_to_first_token === undefined && - chunks.length > 0 - ) { - metrics.time_to_first_token = - getCurrentUnixTimestamp() - startTime; - } - - span.log({ - output, - ...(metadata !== undefined ? { metadata } : {}), - metrics, - }); - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - `Error extracting output for ${channelName}:`, - error, - ); - } finally { - span.end(); - } - }, - onError: (error: Error) => { - span.log({ - error: error.message, - }); - span.end(); - }, - }); - - // Don't delete the span from the map yet - it will be ended by the stream - } else { - // Non-streaming response - try { - const output = config.extractOutput(event.result, event); - const metadata = config.extractMetadata - ? config.extractMetadata(event.result, event) - : undefined; - const metrics = config.extractMetrics( - event.result, - startTime, - event, - ); - - span.log({ - output, - ...(metadata !== undefined ? { metadata } : {}), - metrics, - }); - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(`Error extracting output for ${channelName}:`, error); - } finally { - span.end(); - spans.delete(event); - } - } - }, - - error: (event: any) => { - const spanData = spans.get(event); - if (!spanData) { - return; - } - - const { span } = spanData; - - span.log({ - error: event.error.message, - }); - span.end(); - spans.delete(event); - }, - }; - - channel.subscribe(handlers); - - // Store unsubscribe function - this.unsubscribers.push(() => { - channel.unsubscribe(handlers); - }); - } - - /** - * Subscribe to a channel for sync methods that return event-based streams. - * Used for methods like beta.chat.completions.stream() and responses.stream(). - */ - protected subscribeToSyncStreamChannel( - channelName: string, - config: { - name: string; - type: string; - extractInput: (args: any[]) => { input: any; metadata: any }; - extractFromEvent?: (event: any) => { - output?: any; - metrics?: Record; - metadata?: any; - }; - }, - ): void { - const channel = iso.newTracingChannel(channelName); - - const spans = new WeakMap(); - - const handlers = { - start: (event: StartEvent) => { - const { name, spanAttributes, spanInfoMetadata } = buildStartSpanArgs( - config, - event, - ); - const span = startSpan({ - name, - spanAttributes, - }); - - const startTime = getCurrentUnixTimestamp(); - spans.set(event, { span, startTime }); - - try { - const { input, metadata } = config.extractInput(event.arguments); - span.log({ - input, - metadata: mergeInputMetadata(metadata, spanInfoMetadata), - }); - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(`Error extracting input for ${channelName}:`, error); - } - }, - - end: (event: any) => { - const spanData = spans.get(event); - if (!spanData) { - return; - } - - const { span, startTime } = spanData; - const stream = event.result; - - if (!stream || typeof stream.on !== "function") { - // Not a stream, just end the span - span.end(); - spans.delete(event); - return; - } - - let first = true; - - // Listen for stream events - stream.on("chunk", (chunk: any) => { - if (first) { - const now = getCurrentUnixTimestamp(); - span.log({ - metrics: { - time_to_first_token: now - startTime, - }, - }); - first = false; - } - }); - - stream.on("chatCompletion", (completion: any) => { - try { - span.log({ - output: completion.choices, - }); - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - `Error extracting chatCompletion for ${channelName}:`, - error, - ); - } - }); - - stream.on("event", (streamEvent: any) => { - if (config.extractFromEvent) { - try { - if (first) { - const now = getCurrentUnixTimestamp(); - span.log({ - metrics: { - time_to_first_token: now - startTime, - }, - }); - first = false; - } - - const extracted = config.extractFromEvent(streamEvent); - if (extracted && Object.keys(extracted).length > 0) { - span.log(extracted); - } - } catch (error) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error( - `Error extracting event for ${channelName}:`, - error, - ); - } - } - }); - - stream.on("end", () => { - span.end(); - spans.delete(event); - }); - - // Don't delete the span from the map - it will be deleted when the stream ends - }, - - error: (event: any) => { - const spanData = spans.get(event); - if (!spanData) { - return; - } - - const { span } = spanData; - - span.log({ - error: event.error.message, - }); - span.end(); - spans.delete(event); - }, - }; - - channel.subscribe(handlers); - - // Store unsubscribe function - this.unsubscribers.push(() => { - channel.unsubscribe(handlers); - }); - } -} diff --git a/js/src/instrumentation/core/types.ts b/js/src/instrumentation/core/types.ts index dc2434dc3..0181f3196 100644 --- a/js/src/instrumentation/core/types.ts +++ b/js/src/instrumentation/core/types.ts @@ -27,7 +27,7 @@ export type SpanInfoCarrier< /** * Base context object shared across all events in a trace. */ -export interface BaseContext { +interface BaseContext { /** * Unique identifier for this trace. * Can be used to correlate start/end/error events. @@ -52,42 +52,6 @@ export interface StartEvent extends BaseContext { arguments: TInput[]; } -/** - * Event emitted after the synchronous portion completes. - * For async functions, this fires when the promise is returned (not settled). - */ -export interface EndEvent extends BaseContext { - /** - * The result of the synchronous portion. - * For async functions, this is the promise (not the resolved value). - */ - result: TResult; - - /** - * Arguments passed to the function (also available in StartEvent). - */ - arguments?: unknown[]; -} - -/** - * Event emitted when a function throws or a promise rejects. - */ -export interface ErrorEvent extends BaseContext { - /** - * The error that was thrown or the rejection reason. - */ - error: Error; - - /** - * Arguments passed to the function (also available in StartEvent). - */ - arguments?: unknown[]; -} - -/** - * Event emitted when a promise begins to settle. - * This fires after the synchronous portion and when the async continuation starts. - */ export interface TypedStartEvent< TArguments extends EventArguments = unknown[], > extends BaseContext { @@ -112,17 +76,6 @@ export interface TypedErrorEvent< arguments?: [...TArguments, ...any[]]; } -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface AsyncStartEvent extends StartEvent {} - -/** - * Event emitted when a promise finishes settling. - * This fires BEFORE control returns to user code after await. - * This is where you should extract output data and finalize spans. - */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface AsyncEndEvent extends EndEvent {} - export type StartEventWith< TArguments extends EventArguments = unknown[], TExtra extends object = Record, @@ -144,43 +97,3 @@ export type ErrorEventWith< TArguments extends EventArguments = unknown[], TExtra extends object = Record, > = TypedErrorEvent & TExtra; - -/** - * Subscription handlers for a tracing-compatible global hook. - * - * Common usage pattern: - * - Use start to create spans and extract input - * - Use asyncEnd to extract output and finalize spans - * - Use error to handle failures - */ -export interface ChannelHandlers { - /** - * Called before the synchronous portion of a function executes. - * Use this to create spans and extract input data. - */ - start?: (event: StartEvent) => void; - - /** - * Called after the synchronous portion completes (promise returned). - * Usually not needed for typical instrumentation. - */ - end?: (event: EndEvent) => void; - - /** - * Called when a promise begins to settle. - * Usually not needed for typical instrumentation. - */ - asyncStart?: (event: AsyncStartEvent) => void; - - /** - * Called when a promise finishes settling, before user code continues. - * Use this to extract output, patch streams, and finalize spans. - */ - asyncEnd?: (event: AsyncEndEvent) => void; - - /** - * Called when a function throws or promise rejects. - * Use this to log errors and clean up spans. - */ - error?: (event: ErrorEvent) => void; -} diff --git a/js/src/instrumentation/index.test.ts b/js/src/instrumentation/index.test.ts new file mode 100644 index 000000000..fd5442745 --- /dev/null +++ b/js/src/instrumentation/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import * as instrumentation from "./index"; + +describe("instrumentation public API", () => { + it("exposes only the curated instrumentation APIs", () => { + expect(Object.keys(instrumentation).sort()).toEqual([ + "OpenAIAgentsTraceProcessor", + "braintrustEveInstrumentation", + "braintrustFlueInstrumentation", + "configureInstrumentation", + ]); + }); +}); diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index 8bbbd31ec..e148f2dc5 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -1,42 +1,16 @@ /** * Instrumentation APIs for auto-instrumentation. * - * This module provides the core plugin infrastructure for converting global - * instrumentation hook events into Braintrust spans. - * - * Following the OpenTelemetry pattern, BasePlugin (like InstrumentationBase) - * lives in the core SDK, while individual instrumentation implementations - * can be separate packages. - * * For auto-instrumentation config types, import InstrumentationConfig from the * relevant Braintrust bundler subpath such as `braintrust/vite`. * * @module instrumentation */ -export { BasePlugin } from "./core"; -export { BraintrustPlugin } from "./braintrust-plugin"; -export type { BraintrustPluginConfig } from "./braintrust-plugin"; -export { OpenAIAgentsTraceProcessor } from "./plugins/openai-agents-trace-processor"; -export type { OpenAIAgentsTraceProcessorOptions } from "./plugins/openai-agents-trace-processor"; -export { braintrustFlueInstrumentation } from "./plugins/flue-plugin"; -export { braintrustEveInstrumentation } from "./plugins/eve-plugin"; - -// Re-export core types for external instrumentation packages -export type { - BaseContext, - StartEvent, - EndEvent, - ErrorEvent, - AsyncStartEvent, - AsyncEndEvent, - ChannelHandlers, -} from "./core"; -export { - createChannelName, - parseChannelName, - isValidChannelName, -} from "./core"; +export { OpenAIAgentsTraceProcessor } from "./providers/openai-agents-trace-processor"; +export type { OpenAIAgentsTraceProcessorOptions } from "./providers/openai-agents-trace-processor"; +export { braintrustFlueInstrumentation } from "./providers/flue-instrumentation"; +export { braintrustEveInstrumentation } from "./providers/eve-instrumentation"; // Configuration API export { configureInstrumentation } from "./registry"; diff --git a/js/src/instrumentation/instrumentation-consumers.test.ts b/js/src/instrumentation/instrumentation-consumers.test.ts new file mode 100644 index 000000000..ce4837bb5 --- /dev/null +++ b/js/src/instrumentation/instrumentation-consumers.test.ts @@ -0,0 +1,215 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { InstrumentationIntegrationsConfig } from "./config"; + +const registrations = vi.hoisted(() => ({ + aiSDK: vi.fn(), + anthropic: vi.fn(), + bedrockRuntime: vi.fn(), + claudeAgentSDK: vi.fn(), + cloudflareAgents: vi.fn(), + cloudflareAIChat: vi.fn(), + cloudflareThink: vi.fn(), + cohere: vi.fn(), + cursorSDK: vi.fn(), + genkit: vi.fn(), + gitHubCopilot: vi.fn(), + googleADK: vi.fn(), + googleGenAI: vi.fn(), + groq: vi.fn(), + huggingFace: vi.fn(), + huggingFaceTransformers: vi.fn(), + langChain: vi.fn(), + langSmith: vi.fn(), + mistral: vi.fn(), + ollama: vi.fn(), + openAI: vi.fn(), + openAIAgents: vi.fn(), + openAICodex: vi.fn(), + openRouter: vi.fn(), + openRouterAgent: vi.fn(), + piCodingAgent: vi.fn(), + strandsAgentSDK: vi.fn(), + voyageAI: vi.fn(), +})); + +vi.mock("./providers/ai-sdk-instrumentation", () => ({ + registerAISDKInstrumentation: registrations.aiSDK, +})); +vi.mock("./providers/anthropic-instrumentation", () => ({ + registerAnthropicInstrumentation: registrations.anthropic, +})); +vi.mock("./providers/bedrock-runtime-instrumentation", () => ({ + registerBedrockRuntimeInstrumentation: registrations.bedrockRuntime, +})); +vi.mock("./providers/claude-agent-sdk-instrumentation", () => ({ + registerClaudeAgentSDKInstrumentation: registrations.claudeAgentSDK, +})); +vi.mock("./providers/cloudflare-agents-instrumentation", () => ({ + registerCloudflareAgentsInstrumentation: registrations.cloudflareAgents, +})); +vi.mock("./providers/cloudflare-ai-chat-consumer", () => ({ + registerCloudflareAIChatInstrumentation: registrations.cloudflareAIChat, +})); +vi.mock("./providers/cloudflare-think-instrumentation", () => ({ + registerCloudflareThinkInstrumentation: registrations.cloudflareThink, +})); +vi.mock("./providers/cohere-instrumentation", () => ({ + registerCohereInstrumentation: registrations.cohere, +})); +vi.mock("./providers/cursor-sdk-instrumentation", () => ({ + registerCursorSDKInstrumentation: registrations.cursorSDK, +})); +vi.mock("./providers/genkit-instrumentation", () => ({ + registerGenkitInstrumentation: registrations.genkit, +})); +vi.mock("./providers/github-copilot-instrumentation", () => ({ + registerGitHubCopilotInstrumentation: registrations.gitHubCopilot, +})); +vi.mock("./providers/google-adk-instrumentation", () => ({ + registerGoogleADKInstrumentation: registrations.googleADK, +})); +vi.mock("./providers/google-genai-instrumentation", () => ({ + registerGoogleGenAIInstrumentation: registrations.googleGenAI, +})); +vi.mock("./providers/groq-instrumentation", () => ({ + registerGroqInstrumentation: registrations.groq, +})); +vi.mock("./providers/huggingface-instrumentation", () => ({ + registerHuggingFaceInstrumentation: registrations.huggingFace, +})); +vi.mock("./providers/huggingface-transformers-instrumentation", () => ({ + registerHuggingFaceTransformersInstrumentation: + registrations.huggingFaceTransformers, +})); +vi.mock("./providers/langchain-instrumentation", () => ({ + registerLangChainInstrumentation: registrations.langChain, +})); +vi.mock("./providers/langsmith-instrumentation", () => ({ + registerLangSmithInstrumentation: registrations.langSmith, +})); +vi.mock("./providers/mistral-instrumentation", () => ({ + registerMistralInstrumentation: registrations.mistral, +})); +vi.mock("./providers/ollama-instrumentation", () => ({ + registerOllamaInstrumentation: registrations.ollama, +})); +vi.mock("./providers/openai-instrumentation", () => ({ + registerOpenAIInstrumentation: registrations.openAI, +})); +vi.mock("./providers/openai-agents-instrumentation", () => ({ + registerOpenAIAgentsInstrumentation: registrations.openAIAgents, +})); +vi.mock("./providers/openai-codex-instrumentation", () => ({ + registerOpenAICodexInstrumentation: registrations.openAICodex, +})); +vi.mock("./providers/openrouter-instrumentation", () => ({ + registerOpenRouterInstrumentation: registrations.openRouter, +})); +vi.mock("./providers/openrouter-agent-instrumentation", () => ({ + registerOpenRouterAgentInstrumentation: registrations.openRouterAgent, +})); +vi.mock("./providers/pi-coding-agent-instrumentation", () => ({ + registerPiCodingAgentInstrumentation: registrations.piCodingAgent, +})); +vi.mock("./providers/strands-agent-sdk-instrumentation", () => ({ + registerStrandsAgentSDKInstrumentation: registrations.strandsAgentSDK, +})); +vi.mock("./providers/voyageai-instrumentation", () => ({ + registerVoyageAIInstrumentation: registrations.voyageAI, +})); + +import { registerInstrumentationConsumers } from "./instrumentation-consumers"; + +const integrationCases: Array< + [ + keyof InstrumentationIntegrationsConfig, + Array<(typeof registrations)[keyof typeof registrations]>, + ] +> = [ + ["aisdk", [registrations.aiSDK]], + ["anthropic", [registrations.anthropic]], + ["bedrock", [registrations.bedrockRuntime]], + ["claudeAgentSDK", [registrations.claudeAgentSDK]], + ["cloudflareAgents", [registrations.cloudflareAgents]], + ["cloudflareAIChat", [registrations.cloudflareAIChat]], + ["cloudflareThink", [registrations.cloudflareThink]], + ["cohere", [registrations.cohere]], + ["cursorSDK", [registrations.cursorSDK]], + ["genkit", [registrations.genkit]], + ["gitHubCopilot", [registrations.gitHubCopilot]], + ["googleADK", [registrations.googleADK]], + ["googleGenAI", [registrations.googleGenAI]], + ["groq", [registrations.groq]], + [ + "huggingface", + [registrations.huggingFace, registrations.huggingFaceTransformers], + ], + ["langchain", [registrations.langChain]], + ["langsmith", [registrations.langSmith]], + ["mistral", [registrations.mistral]], + ["ollama", [registrations.ollama]], + ["openai", [registrations.openAI]], + ["openAIAgents", [registrations.openAIAgents]], + ["openaiCodexSDK", [registrations.openAICodex]], + ["openrouter", [registrations.openRouter]], + ["openrouterAgent", [registrations.openRouterAgent]], + ["piCodingAgent", [registrations.piCodingAgent]], + ["strandsAgentSDK", [registrations.strandsAgentSDK]], + ["voyageai", [registrations.voyageAI]], +]; + +describe("registerInstrumentationConsumers", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("registers every instrumentation consumer by default", () => { + registerInstrumentationConsumers(); + + for (const register of Object.values(registrations)) { + expect(register).toHaveBeenCalledTimes(1); + } + expect(registrations.langSmith).toHaveBeenCalledWith({ + skipLangChainRuns: true, + }); + }); + + it.each(integrationCases)( + "does not register the %s integration when disabled", + (integration, disabledRegistrations) => { + registerInstrumentationConsumers({ + integrations: { [integration]: false }, + }); + + for (const register of disabledRegistrations) { + expect(register).not.toHaveBeenCalled(); + } + }, + ); + + it.each([ + ["vercel", registrations.aiSDK], + ["google", registrations.googleGenAI], + ["cursor", registrations.cursorSDK], + ["awsBedrock", registrations.bedrockRuntime], + ["awsBedrockRuntime", registrations.bedrockRuntime], + ["langgraph", registrations.langChain], + ] as const)( + "honors the legacy or related %s disable flag", + (integration, register) => { + registerInstrumentationConsumers({ + integrations: { [integration]: false }, + }); + + expect(register).not.toHaveBeenCalled(); + }, + ); + + it("tells LangSmith not to suppress LangChain runs when LangChain is disabled", () => { + registerInstrumentationConsumers({ integrations: { langchain: false } }); + + expect(registrations.langSmith).toHaveBeenCalledWith({ + skipLangChainRuns: false, + }); + }); +}); diff --git a/js/src/instrumentation/instrumentation-consumers.ts b/js/src/instrumentation/instrumentation-consumers.ts new file mode 100644 index 000000000..ed32522a9 --- /dev/null +++ b/js/src/instrumentation/instrumentation-consumers.ts @@ -0,0 +1,159 @@ +import { registerOpenAIInstrumentation } from "./providers/openai-instrumentation"; +import { registerOpenAICodexInstrumentation } from "./providers/openai-codex-instrumentation"; +import { registerAnthropicInstrumentation } from "./providers/anthropic-instrumentation"; +import { registerAISDKInstrumentation } from "./providers/ai-sdk-instrumentation"; +import { registerClaudeAgentSDKInstrumentation } from "./providers/claude-agent-sdk-instrumentation"; +import { registerCloudflareThinkInstrumentation } from "./providers/cloudflare-think-instrumentation"; +import { registerCursorSDKInstrumentation } from "./providers/cursor-sdk-instrumentation"; +import { registerOpenAIAgentsInstrumentation } from "./providers/openai-agents-instrumentation"; +import { registerGoogleGenAIInstrumentation } from "./providers/google-genai-instrumentation"; +import { registerHuggingFaceInstrumentation } from "./providers/huggingface-instrumentation"; +import { registerHuggingFaceTransformersInstrumentation } from "./providers/huggingface-transformers-instrumentation"; +import { registerOpenRouterAgentInstrumentation } from "./providers/openrouter-agent-instrumentation"; +import { registerOpenRouterInstrumentation } from "./providers/openrouter-instrumentation"; +import { registerMistralInstrumentation } from "./providers/mistral-instrumentation"; +import { registerOllamaInstrumentation } from "./providers/ollama-instrumentation"; +import { registerGoogleADKInstrumentation } from "./providers/google-adk-instrumentation"; +import { registerCohereInstrumentation } from "./providers/cohere-instrumentation"; +import { registerGroqInstrumentation } from "./providers/groq-instrumentation"; +import { registerBedrockRuntimeInstrumentation } from "./providers/bedrock-runtime-instrumentation"; +import { registerGenkitInstrumentation } from "./providers/genkit-instrumentation"; +import { registerGitHubCopilotInstrumentation } from "./providers/github-copilot-instrumentation"; +import { registerLangChainInstrumentation } from "./providers/langchain-instrumentation"; +import { registerLangSmithInstrumentation } from "./providers/langsmith-instrumentation"; +import { registerPiCodingAgentInstrumentation } from "./providers/pi-coding-agent-instrumentation"; +import { registerStrandsAgentSDKInstrumentation } from "./providers/strands-agent-sdk-instrumentation"; +import { registerVoyageAIInstrumentation } from "./providers/voyageai-instrumentation"; +import { registerCloudflareAIChatInstrumentation } from "./providers/cloudflare-ai-chat-consumer"; +import { registerCloudflareAgentsInstrumentation } from "./providers/cloudflare-agents-instrumentation"; +import type { InstrumentationConfig } from "./config"; + +/** Registers the configured process-lifetime instrumentation consumers. */ +export function registerInstrumentationConsumers( + config: InstrumentationConfig = {}, +): void { + const integrations = config.integrations ?? {}; + + if (integrations.openai !== false) { + registerOpenAIInstrumentation(); + } + + if (integrations.openaiCodexSDK !== false) { + registerOpenAICodexInstrumentation(); + } + + if (integrations.anthropic !== false) { + registerAnthropicInstrumentation(); + } + + // Support both 'aisdk' and legacy 'vercel' config keys + if (integrations.aisdk !== false && integrations.vercel !== false) { + registerAISDKInstrumentation(); + } + + if (integrations.claudeAgentSDK !== false) { + registerClaudeAgentSDKInstrumentation(); + } + + if (integrations.cloudflareThink !== false) { + registerCloudflareThinkInstrumentation(); + } + + if (integrations.cursorSDK !== false && integrations.cursor !== false) { + registerCursorSDKInstrumentation(); + } + + if (integrations.openAIAgents !== false) { + registerOpenAIAgentsInstrumentation(); + } + + // Support both 'googleGenAI' and legacy 'google' config keys + if (integrations.googleGenAI !== false && integrations.google !== false) { + registerGoogleGenAIInstrumentation(); + } + + if (integrations.huggingface !== false) { + registerHuggingFaceInstrumentation(); + registerHuggingFaceTransformersInstrumentation(); + } + + if (integrations.openrouter !== false) { + registerOpenRouterInstrumentation(); + } + + if (integrations.openrouterAgent !== false) { + registerOpenRouterAgentInstrumentation(); + } + + if (integrations.mistral !== false) { + registerMistralInstrumentation(); + } + + if (integrations.ollama !== false) { + registerOllamaInstrumentation(); + } + + if (integrations.googleADK !== false) { + registerGoogleADKInstrumentation(); + } + + if (integrations.cohere !== false) { + registerCohereInstrumentation(); + } + + if (integrations.voyageai !== false) { + registerVoyageAIInstrumentation(); + } + + if (integrations.groq !== false) { + registerGroqInstrumentation(); + } + + if ( + integrations.bedrock !== false && + integrations.awsBedrock !== false && + integrations.awsBedrockRuntime !== false + ) { + registerBedrockRuntimeInstrumentation(); + } + + if (integrations.genkit !== false) { + registerGenkitInstrumentation(); + } + + if (integrations.gitHubCopilot !== false) { + registerGitHubCopilotInstrumentation(); + } + + if (integrations.piCodingAgent !== false) { + registerPiCodingAgentInstrumentation(); + } + + if (integrations.strandsAgentSDK !== false) { + registerStrandsAgentSDKInstrumentation(); + } + + if (integrations.cloudflareAIChat !== false) { + registerCloudflareAIChatInstrumentation(); + } + + if (integrations.cloudflareAgents !== false) { + registerCloudflareAgentsInstrumentation(); + } + + if (integrations.langchain !== false && integrations.langgraph !== false) { + registerLangChainInstrumentation(); + } + + if (integrations.langsmith !== false) { + registerLangSmithInstrumentation({ + skipLangChainRuns: integrations.langchain !== false, + }); + } + + // Mastra is intentionally not wired here: `@mastra/core` ships its own + // ObservabilityExporter contract, and `BraintrustObservabilityExporter` + // (wrappers/mastra.ts) is auto-installed by the loader patch in + // `auto-instrumentations/loader/mastra-observability-patch.ts` rather than + // by an instrumentation consumer / global hook subscription. +} diff --git a/js/src/instrumentation/plugins/cloudflare-agents-plugin.ts b/js/src/instrumentation/plugins/cloudflare-agents-plugin.ts deleted file mode 100644 index d9813f419..000000000 --- a/js/src/instrumentation/plugins/cloudflare-agents-plugin.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { debugLogger } from "../../debug-logger"; -import type { IsoChannelHandlers } from "../../isomorph"; -import { _internalStartSpanWithContext } from "../../logger"; -import type { Span } from "../../logger"; -import { - INSTRUMENTATION_NAMES, - withSpanInstrumentationName, -} from "../../span-origin"; -import { SpanTypeAttribute } from "../../../util/index"; -import { BasePlugin } from "../core"; -import type { ChannelMessage } from "../core/channel-definitions"; -import { cloudflareAgentsChannels } from "./cloudflare-agents-channels"; - -const CLOUDFLARE_WORKERS_CONTEXT = { - span_origin: { - environment: { type: "server", name: "cloudflare_workers" }, - }, -}; - -export class CloudflareAgentsPlugin extends BasePlugin { - protected onEnable(): void { - const channel = cloudflareAgentsChannels.runAgentTool.tracingChannel(); - const spans = new WeakMap(); - const handlers: IsoChannelHandlers< - ChannelMessage - > = { - start: (event) => { - try { - const agentClass = event.arguments[0]; - const options = event.arguments[1]; - if (ownValue(options, "detached")) { - return; - } - - const name = ownValue(agentClass, "name"); - if (typeof name !== "string" || name.length === 0) { - debugLogger.warn( - "Skipping Cloudflare Agents runAgentTool span because the child agent class has no name.", - ); - return; - } - - const span = _internalStartSpanWithContext( - withSpanInstrumentationName( - { - name, - spanAttributes: { type: SpanTypeAttribute.TOOL }, - event: { - input: ownValue(options, "input"), - }, - }, - INSTRUMENTATION_NAMES.CLOUDFLARE_AGENTS, - ), - CLOUDFLARE_WORKERS_CONTEXT, - ); - spans.set(event, span); - } catch (error) { - logInstrumentationError("start", error); - } - }, - asyncEnd: (event) => { - const span = spans.get(event); - if (!span) { - return; - } - spans.delete(event); - - try { - const status = ownValue(event.result, "status"); - if (status === "completed") { - span.log({ output: ownValue(event.result, "output") }); - } else { - const error = ownValue(event.result, "error"); - if (typeof error === "string") { - span.log({ error }); - } - } - } catch (error) { - logInstrumentationError("completion", error); - } finally { - safelyEndSpan(span); - } - }, - error: (event) => { - const span = spans.get(event); - if (!span) { - return; - } - spans.delete(event); - - try { - span.log({ error: event.error }); - } catch (error) { - logInstrumentationError("rejection", error); - } finally { - safelyEndSpan(span); - } - }, - }; - - channel.subscribe(handlers); - this.unsubscribers.push(() => channel.unsubscribe(handlers)); - } - - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - } -} - -function ownValue(value: unknown, key: PropertyKey): unknown { - if (!isObjectLike(value)) { - return undefined; - } - const descriptor = Object.getOwnPropertyDescriptor(value, key); - return descriptor && "value" in descriptor ? descriptor.value : undefined; -} - -function isObjectLike(value: unknown): value is object { - return ( - (typeof value === "object" && value !== null) || typeof value === "function" - ); -} - -function safelyEndSpan(span: Span): void { - try { - span.end(); - } catch (error) { - logInstrumentationError("span end", error); - } -} - -function logInstrumentationError(operation: string, error: unknown): void { - debugLogger.error( - `Failed to process Cloudflare Agents ${operation} instrumentation:`, - error, - ); -} diff --git a/js/src/instrumentation/plugins/openai-plugin.ts b/js/src/instrumentation/plugins/openai-plugin.ts deleted file mode 100644 index d1ca89dcc..000000000 --- a/js/src/instrumentation/plugins/openai-plugin.ts +++ /dev/null @@ -1,613 +0,0 @@ -import { BasePlugin } from "../core"; -import { - traceAsyncChannel, - traceStreamingChannel, - traceSyncStreamChannel, - unsubscribeAll, -} from "../core/channel-tracing"; -import { Attachment } from "../../logger"; -import { SpanTypeAttribute, isObject } from "../../../util/index"; -import { getCurrentUnixTimestamp } from "../../util"; -import { processInputAttachments } from "../../wrappers/attachment-utils"; -import { openAIChannels } from "./openai-channels"; -import { - BRAINTRUST_CACHED_STREAM_METRIC, - getCachedMetricFromHeaders, - parseMetricsFromUsage, -} from "../../openai-utils"; -import type { - OpenAIChatChoice, - OpenAIChatCompletionChunk, - OpenAIChatLogprobs, - OpenAIResponseStreamEvent, -} from "../../vendor-sdk-types/openai"; - -/** - * Plugin for OpenAI SDK instrumentation. - * - * Handles instrumentation for: - * - Chat completions (streaming and non-streaming) - * - Embeddings - * - Moderations - * - Beta API (parse, stream) - * - Responses API (create, stream, parse, compact) - */ -export class OpenAIPlugin extends BasePlugin { - constructor() { - super(); - } - - protected onEnable(): void { - // Chat Completions - supports streaming - this.unsubscribers.push( - traceStreamingChannel(openAIChannels.chatCompletionsCreate, { - name: "Chat Completion", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { messages, ...metadata } = params; - return { - input: processInputAttachments(messages), - metadata: { ...metadata, provider: "openai" }, - }; - }, - extractOutput: (result) => { - return result?.choices; - }, - extractMetrics: (result, startTime, endEvent) => { - const metrics = withCachedMetric( - parseMetricsFromUsage(result?.usage), - result, - endEvent, - ); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - aggregateChunks: aggregateChatCompletionChunks, - }), - ); - - // Embeddings - this.unsubscribers.push( - traceAsyncChannel(openAIChannels.embeddingsCreate, { - name: "Embedding", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { input, ...metadata } = params; - return { - input, - metadata: { ...metadata, provider: "openai" }, - }; - }, - extractOutput: (result) => { - const embedding = result?.data?.[0]?.embedding; - return Array.isArray(embedding) - ? { embedding_length: embedding.length } - : undefined; - }, - extractMetrics: (result, _startTime, endEvent) => { - return withCachedMetric( - parseMetricsFromUsage(result?.usage), - result, - endEvent, - ); - }, - }), - ); - - // Beta Chat Completions Parse - this.unsubscribers.push( - traceStreamingChannel(openAIChannels.betaChatCompletionsParse, { - name: "Chat Completion", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { messages, ...metadata } = params; - return { - input: processInputAttachments(messages), - metadata: { ...metadata, provider: "openai" }, - }; - }, - extractOutput: (result) => { - return result?.choices; - }, - extractMetrics: (result, startTime, endEvent) => { - const metrics = withCachedMetric( - parseMetricsFromUsage(result?.usage), - result, - endEvent, - ); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - aggregateChunks: aggregateChatCompletionChunks, - }), - ); - - // Beta Chat Completions Stream (sync method returning event-based stream) - this.unsubscribers.push( - traceSyncStreamChannel(openAIChannels.betaChatCompletionsStream, { - name: "Chat Completion", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { messages, ...metadata } = params; - return { - input: processInputAttachments(messages), - metadata: { ...metadata, provider: "openai" }, - }; - }, - }), - ); - - // Moderations - this.unsubscribers.push( - traceAsyncChannel(openAIChannels.moderationsCreate, { - name: "Moderation", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { input, ...metadata } = params; - return { - input, - metadata: { ...metadata, provider: "openai" }, - }; - }, - extractOutput: (result) => { - return result?.results; - }, - extractMetrics: (result, _startTime, endEvent) => { - return withCachedMetric( - parseMetricsFromUsage(result?.usage), - result, - endEvent, - ); - }, - }), - ); - - // Responses API - create (supports streaming via stream=true param) - this.unsubscribers.push( - traceStreamingChannel(openAIChannels.responsesCreate, { - name: "openai.responses.create", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { input, ...metadata } = params; - return { - input: processInputAttachments(input), - metadata: { ...metadata, provider: "openai" }, - }; - }, - extractOutput: (result) => { - return processImagesInOutput(result?.output); - }, - extractMetadata: (result) => { - if (!result) { - return undefined; - } - const { output: _output, usage: _usage, ...metadata } = result; - return Object.keys(metadata).length > 0 ? metadata : undefined; - }, - extractMetrics: (result, startTime, endEvent) => { - const metrics = withCachedMetric( - parseMetricsFromUsage(result?.usage), - result, - endEvent, - ); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - aggregateChunks: aggregateResponseStreamEvents, - }), - ); - - // Responses API - stream (sync method returning event-based stream) - this.unsubscribers.push( - traceSyncStreamChannel(openAIChannels.responsesStream, { - name: "openai.responses.create", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { input, ...metadata } = params; - return { - input: processInputAttachments(input), - metadata: { ...metadata, provider: "openai" }, - }; - }, - extractFromEvent: (event) => { - if (event.type !== "response.completed" || !event.response) { - return {}; - } - - const response = event.response; - const data: Record = {}; - - if (response.output !== undefined) { - data.output = processImagesInOutput(response.output); - } - - const { usage: _usage, output: _output, ...metadata } = response; - if (Object.keys(metadata).length > 0) { - data.metadata = metadata; - } - - data.metrics = parseMetricsFromUsage(response.usage); - return data; - }, - }), - ); - - // Responses API - parse - this.unsubscribers.push( - traceStreamingChannel(openAIChannels.responsesParse, { - name: "openai.responses.parse", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { input, ...metadata } = params; - return { - input: processInputAttachments(input), - metadata: { ...metadata, provider: "openai" }, - }; - }, - extractOutput: (result) => { - return processImagesInOutput(result?.output); - }, - extractMetadata: (result) => { - if (!result) { - return undefined; - } - const { output: _output, usage: _usage, ...metadata } = result; - return Object.keys(metadata).length > 0 ? metadata : undefined; - }, - extractMetrics: (result, startTime, endEvent) => { - const metrics = withCachedMetric( - parseMetricsFromUsage(result?.usage), - result, - endEvent, - ); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - aggregateChunks: aggregateResponseStreamEvents, - }), - ); - - // Responses API - compact - this.unsubscribers.push( - traceAsyncChannel(openAIChannels.responsesCompact, { - name: "openai.responses.compact", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { input, ...metadata } = params; - return { - input: processInputAttachments(input), - metadata: { ...metadata, provider: "openai" }, - }; - }, - extractOutput: (result) => { - return processImagesInOutput(result?.output); - }, - extractMetadata: (result) => { - if (!result) { - return undefined; - } - const { output: _output, usage: _usage, ...metadata } = result; - return Object.keys(metadata).length > 0 ? metadata : undefined; - }, - extractMetrics: (result, startTime, endEvent) => { - const metrics = withCachedMetric( - parseMetricsFromUsage(result?.usage), - result, - endEvent, - ); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - }), - ); - } - - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } -} - -function getCachedMetricFromEndEvent(endEvent: unknown): number | undefined { - if (!isObject(endEvent)) { - return undefined; - } - - const response = (endEvent as Record).response; - if (!isObject(response)) { - return undefined; - } - - const headers = (response as { headers?: unknown }).headers; - if (!headers || typeof (headers as Headers).get !== "function") { - return undefined; - } - - return getCachedMetricFromHeaders(headers as Headers); -} - -function withCachedMetric( - metrics: Record, - result: unknown, - endEvent?: unknown, -): Record { - if (metrics.cached !== undefined) { - return metrics; - } - - const cachedFromEvent = getCachedMetricFromEndEvent(endEvent); - if (cachedFromEvent !== undefined) { - return { - ...metrics, - cached: cachedFromEvent, - }; - } - - if (!isObject(result)) { - return metrics; - } - - const cached = (result as Record)[ - BRAINTRUST_CACHED_STREAM_METRIC - ]; - - if (typeof cached !== "number") { - return metrics; - } - - return { - ...metrics, - cached, - }; -} - -/** - * Process output to convert base64 images to attachments. - * Used for Responses API image generation output. - */ -export function processImagesInOutput(output: any): any { - if (Array.isArray(output)) { - return output.map(processImagesInOutput); - } - - if (isObject(output)) { - if ( - output.type === "image_generation_call" && - output.result && - typeof output.result === "string" - ) { - const fileExtension = output.output_format || "png"; - const contentType = `image/${fileExtension}`; - - const baseFilename = - output.revised_prompt && typeof output.revised_prompt === "string" - ? output.revised_prompt.slice(0, 50).replace(/[^a-zA-Z0-9]/g, "_") - : "generated_image"; - const filename = `${baseFilename}.${fileExtension}`; - - // Convert base64 string to Blob - const binaryString = atob(output.result); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - const blob = new Blob([bytes], { type: contentType }); - - const attachment = new Attachment({ - data: blob, - filename: filename, - contentType: contentType, - }); - - return { - ...output, - result: attachment, - }; - } - } - - return output; -} - -function mergeLogprobTokens( - existing: OpenAIChatLogprobs["content"] | OpenAIChatLogprobs["refusal"], - incoming: OpenAIChatLogprobs["content"] | OpenAIChatLogprobs["refusal"], -): OpenAIChatLogprobs["content"] | OpenAIChatLogprobs["refusal"] { - if (incoming === undefined) { - return existing; - } - - if (incoming === null) { - return existing ?? null; - } - - if (Array.isArray(existing)) { - return [...existing, ...incoming]; - } - - return [...incoming]; -} - -function aggregateChatLogprobs( - existing: OpenAIChatLogprobs | null | undefined, - incoming: OpenAIChatLogprobs | null | undefined, -): OpenAIChatLogprobs | null | undefined { - if (incoming === undefined) { - return existing; - } - - if (incoming === null) { - return existing ?? null; - } - - const aggregated: OpenAIChatLogprobs = - existing && existing !== null - ? { ...existing, ...incoming } - : { ...incoming }; - - const content = mergeLogprobTokens(existing?.content, incoming.content); - if (content !== undefined) { - aggregated.content = content; - } - - const refusal = mergeLogprobTokens(existing?.refusal, incoming.refusal); - if (refusal !== undefined) { - aggregated.refusal = refusal; - } - - return aggregated; -} - -/** - * Aggregate chat completion chunks into a single response. - * Combines role (first), content (concatenated), tool_calls (by id), - * finish_reason (last), and usage (last chunk). - */ -export function aggregateChatCompletionChunks( - chunks: OpenAIChatCompletionChunk[], - streamResult?: unknown, - endEvent?: unknown, -): { - output: OpenAIChatChoice[]; - metrics: Record; -} { - let role = undefined; - let content = undefined; - let refusal = undefined; - let tool_calls = undefined; - let logprobs: OpenAIChatLogprobs | null | undefined = undefined; - let finish_reason = undefined; - let metrics: Record = {}; - - for (const chunk of chunks) { - if (chunk.usage) { - metrics = { - ...metrics, - ...parseMetricsFromUsage(chunk.usage), - }; - } - - const choice = chunk.choices?.[0]; - if (!choice) { - continue; - } - - if (choice.finish_reason) { - finish_reason = choice.finish_reason; - } - - logprobs = aggregateChatLogprobs(logprobs, choice.logprobs); - - const delta = choice.delta; - if (!delta) { - continue; - } - - if (delta.finish_reason) { - finish_reason = delta.finish_reason; - } - - if (!role && delta.role) { - role = delta.role; - } - - if (delta.content) { - content = (content || "") + delta.content; - } - - if (delta.refusal) { - refusal = (refusal || "") + delta.refusal; - } - - if (delta.tool_calls) { - const toolDelta = delta.tool_calls[0]; - if ( - !tool_calls || - (toolDelta.id && tool_calls[tool_calls.length - 1].id !== toolDelta.id) - ) { - tool_calls = [ - ...(tool_calls || []), - { - id: toolDelta.id, - type: toolDelta.type, - function: toolDelta.function, - }, - ]; - } else { - tool_calls[tool_calls.length - 1].function.arguments += - toolDelta.function.arguments; - } - } - } - - metrics = withCachedMetric(metrics, streamResult, endEvent); - - return { - metrics, - output: [ - { - index: 0, - message: { - role, - content, - ...(refusal !== undefined ? { refusal } : {}), - tool_calls, - }, - logprobs: logprobs ?? null, - finish_reason, - }, - ], - }; -} - -function aggregateResponseStreamEvents( - chunks: OpenAIResponseStreamEvent[], - _streamResult?: unknown, - endEvent?: unknown, -): { - output: any; - metrics: Record; - metadata?: Record; -} { - let output: any = undefined; - let metrics: Record = {}; - let metadata: Record | undefined = undefined; - - for (const chunk of chunks) { - if (!chunk || !chunk.type || !chunk.response) { - continue; - } - if (chunk.type !== "response.completed") { - continue; - } - - const response = chunk.response; - if (response?.output !== undefined) { - output = processImagesInOutput(response.output); - } - - const { usage: _usage, output: _output, ...rest } = response || {}; - if (Object.keys(rest).length > 0) { - metadata = rest; - } - - metrics = parseMetricsFromUsage(response?.usage); - } - - return { - output, - metrics: withCachedMetric(metrics, undefined, endEvent), - ...(metadata !== undefined ? { metadata } : {}), - }; -} - -export { parseMetricsFromUsage }; diff --git a/js/src/instrumentation/plugins/ai-sdk-channels.ts b/js/src/instrumentation/providers/ai-sdk-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/ai-sdk-channels.ts rename to js/src/instrumentation/providers/ai-sdk-channels.ts diff --git a/js/src/instrumentation/plugins/ai-sdk-plugin.streaming.test.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts similarity index 92% rename from js/src/instrumentation/plugins/ai-sdk-plugin.streaming.test.ts rename to js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts index 8803708c3..1921dfa02 100644 --- a/js/src/instrumentation/plugins/ai-sdk-plugin.streaming.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts @@ -22,7 +22,6 @@ import { } from "../../wrappers/ai-sdk/harness-agent-context"; import { workflowAgentWrapperSpanCountForTesting } from "../../wrappers/ai-sdk/workflow-agent-context"; import { aiSDKChannels } from "./ai-sdk-channels"; -import { AISDKPlugin } from "./ai-sdk-plugin"; try { configureNode(); @@ -41,7 +40,7 @@ describe("AI SDK streaming instrumentation", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "ai-sdk-plugin.streaming.test.ts", + projectName: "ai-sdk-instrumentation.streaming.test.ts", projectId: "test-project-id", }); }); @@ -890,144 +889,130 @@ describe("AI SDK streaming instrumentation", () => { test("baseStream patch preserves derived stream getters", async () => { expect(await backgroundLogger.drain()).toHaveLength(0); - const plugin = new AISDKPlugin(); - plugin.enable(); - - try { - let chunkSent = false; - const result = (await aiSDKChannels.streamText.tracePromise( - async () => { - const resultRecord = { - baseStream: new ReadableStream({ - pull(controller) { - if (chunkSent) { - controller.close(); - return; - } - - chunkSent = true; - controller.enqueue({ - type: "text-delta", - id: "text-1", - delta: "fresh", - }); - }, - }), - text: Promise.resolve("fresh"), - } as any; - - Object.defineProperty(resultRecord, "textStream", { - configurable: true, - enumerable: true, - get() { - const [textBranch, baseBranch] = this.baseStream.tee(); - this.baseStream = baseBranch; - return textBranch.pipeThrough( - new TransformStream({ - transform(chunk: any, controller) { - if (chunk.type === "text-delta") { - controller.enqueue(chunk.delta); - } - }, - }), - ); - }, - }); + let chunkSent = false; + const result = (await aiSDKChannels.streamText.tracePromise( + async () => { + const resultRecord = { + baseStream: new ReadableStream({ + pull(controller) { + if (chunkSent) { + controller.close(); + return; + } - return resultRecord; - }, - { - arguments: [ - { - model: "mock-stream-model", - prompt: "Reply with fresh.", + chunkSent = true; + controller.enqueue({ + type: "text-delta", + id: "text-1", + delta: "fresh", + }); }, - ], - } as any, - )) as any; + }), + text: Promise.resolve("fresh"), + } as any; + + Object.defineProperty(resultRecord, "textStream", { + configurable: true, + enumerable: true, + get() { + const [textBranch, baseBranch] = this.baseStream.tee(); + this.baseStream = baseBranch; + return textBranch.pipeThrough( + new TransformStream({ + transform(chunk: any, controller) { + if (chunk.type === "text-delta") { + controller.enqueue(chunk.delta); + } + }, + }), + ); + }, + }); - expect( - Object.getOwnPropertyDescriptor(result, "textStream")?.get, - ).toEqual(expect.any(Function)); + return resultRecord; + }, + { + arguments: [ + { + model: "mock-stream-model", + prompt: "Reply with fresh.", + }, + ], + } as any, + )) as any; - let firstText = ""; - for await (const chunk of result.textStream) { - firstText += chunk; - } + expect(Object.getOwnPropertyDescriptor(result, "textStream")?.get).toEqual( + expect.any(Function), + ); - let secondText = ""; - for await (const chunk of result.textStream) { - secondText += chunk; - } + let firstText = ""; + for await (const chunk of result.textStream) { + firstText += chunk; + } - expect(firstText).toBe("fresh"); - expect(secondText).toBe("fresh"); - } finally { - plugin.disable(); + let secondText = ""; + for await (const chunk of result.textStream) { + secondText += chunk; } + + expect(firstText).toBe("fresh"); + expect(secondText).toBe("fresh"); }); test("async iterable stream accessors preserve ReadableStream methods", async () => { expect(await backgroundLogger.drain()).toHaveLength(0); - const plugin = new AISDKPlugin(); - plugin.enable(); - - try { - const result = (await aiSDKChannels.streamText.tracePromise( - async () => { - const resultRecord = { - stream: new ReadableStream({ - start(controller) { - controller.enqueue("v7"); - controller.close(); - }, - }), - text: Promise.resolve("v7"), - } as any; - - Object.defineProperty(resultRecord, "textStream", { - configurable: true, - enumerable: true, - get() { - return this.stream.pipeThrough( - new TransformStream({ - transform(chunk: string, controller) { - controller.enqueue(chunk.toUpperCase()); - }, - }), - ); + const result = (await aiSDKChannels.streamText.tracePromise( + async () => { + const resultRecord = { + stream: new ReadableStream({ + start(controller) { + controller.enqueue("v7"); + controller.close(); }, - }); + }), + text: Promise.resolve("v7"), + } as any; + + Object.defineProperty(resultRecord, "textStream", { + configurable: true, + enumerable: true, + get() { + return this.stream.pipeThrough( + new TransformStream({ + transform(chunk: string, controller) { + controller.enqueue(chunk.toUpperCase()); + }, + }), + ); + }, + }); - return resultRecord; - }, - { - arguments: [ - { - model: "mock-v7-stream-model", - prompt: "Reply with v7.", - }, - ], - } as any, - )) as any; + return resultRecord; + }, + { + arguments: [ + { + model: "mock-v7-stream-model", + prompt: "Reply with v7.", + }, + ], + } as any, + )) as any; - expect(result.stream.pipeThrough).toEqual(expect.any(Function)); - expect(result.stream.getReader).toEqual(expect.any(Function)); + expect(result.stream.pipeThrough).toEqual(expect.any(Function)); + expect(result.stream.getReader).toEqual(expect.any(Function)); - const textStream = result.textStream; - expect(textStream.pipeThrough).toEqual(expect.any(Function)); - expect(textStream.getReader).toEqual(expect.any(Function)); + const textStream = result.textStream; + expect(textStream.pipeThrough).toEqual(expect.any(Function)); + expect(textStream.getReader).toEqual(expect.any(Function)); - const reader = textStream.getReader(); - const first = await reader.read(); - const second = await reader.read(); + const reader = textStream.getReader(); + const first = await reader.read(); + const second = await reader.read(); - expect(first).toEqual({ done: false, value: "V7" }); - expect(second).toEqual({ done: true, value: undefined }); - } finally { - plugin.disable(); - } + expect(first).toEqual({ done: false, value: "V7" }); + expect(second).toEqual({ done: true, value: undefined }); }); test("wrapAgentClass instruments all HarnessAgent turn methods without serializing sessions", async () => { diff --git a/js/src/instrumentation/plugins/ai-sdk-plugin.test.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts similarity index 96% rename from js/src/instrumentation/plugins/ai-sdk-plugin.test.ts rename to js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts index fcc538deb..60fcbc4bf 100644 --- a/js/src/instrumentation/plugins/ai-sdk-plugin.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; const telemetryMocks = vi.hoisted(() => ({ braintrustAISDKTelemetry: vi.fn(), @@ -23,13 +23,13 @@ vi.mock("../../wrappers/ai-sdk/telemetry", () => ({ })); import { - AISDKPlugin, + registerAISDKInstrumentation, DEFAULT_DENY_OUTPUT_PATHS, processAISDKCallInput, processAISDKWorkflowAgentCallInput, processAISDKWorkflowAgentModelCallInput, processAISDKOutput as processAISDKOutputActual, -} from "./ai-sdk-plugin"; +} from "./ai-sdk-instrumentation"; import iso from "../../isomorph"; import { serializeAISDKToolsForLogging } from "../../wrappers/ai-sdk/tool-serialization"; import { BRAINTRUST_AI_SDK_V7_OPERATION_KEY as AI_SDK_V7_OPERATION_KEY } from "../../vendor-sdk-types/ai-sdk-v7-telemetry"; @@ -39,7 +39,6 @@ type MockTracingChannel = { handlers: any[]; hasSubscribers: boolean; subscribe: ReturnType; - unsubscribe: ReturnType; }; const mockChannels = new Map(); @@ -47,9 +46,7 @@ const mockChannels = new Map(); // Since these are private, we'll test them through the public API // But we'll also add some tests for the exported utility functions -describe("AISDKPlugin", () => { - let plugin: AISDKPlugin; - +describe("registerAISDKInstrumentation", () => { beforeEach(() => { mockChannels.clear(); telemetryMocks.telemetry = { @@ -70,39 +67,10 @@ describe("AISDKPlugin", () => { channel.handlers.push(handlers); channel.hasSubscribers = true; }), - unsubscribe: vi.fn((handlers: any) => { - channel.handlers = channel.handlers.filter( - (candidate) => candidate !== handlers, - ); - channel.hasSubscribers = channel.handlers.length > 0; - return true; - }), }; mockChannels.set(name, channel); return channel; }); - - plugin = new AISDKPlugin(); - }); - - afterEach(() => { - if (plugin) { - plugin.disable(); - } - }); - - describe("constructor", () => { - it("should create plugin with default config", () => { - const defaultPlugin = new AISDKPlugin(); - expect(defaultPlugin).toBeInstanceOf(AISDKPlugin); - }); - - it("should create plugin with custom config", () => { - const customPlugin = new AISDKPlugin({ - denyOutputPaths: ["custom.path"], - }); - expect(customPlugin).toBeInstanceOf(AISDKPlugin); - }); }); describe("WorkflowAgent input extraction", () => { @@ -192,33 +160,9 @@ describe("AISDKPlugin", () => { }); }); - describe("enable/disable", () => { - it("should enable plugin", () => { - expect(() => plugin.enable()).not.toThrow(); - }); - - it("should disable plugin", () => { - plugin.enable(); - expect(() => plugin.disable()).not.toThrow(); - }); - - it("should handle multiple enable calls", () => { - plugin.enable(); - expect(() => plugin.enable()).not.toThrow(); - }); - - it("should handle multiple disable calls", () => { - plugin.enable(); - plugin.disable(); - expect(() => plugin.disable()).not.toThrow(); - }); - - it("should unsubscribe from channels on disable", () => { - plugin.enable(); - plugin.disable(); - // Verify that unsubscribers were called - // This is tested indirectly - if it doesn't throw, unsubscribe worked - expect(true).toBe(true); + describe("registration", () => { + it("registers without throwing", () => { + expect(() => registerAISDKInstrumentation()).not.toThrow(); }); }); @@ -238,7 +182,7 @@ describe("AISDKPlugin", () => { onStepEnd: existingOnStepEnd, }; - plugin.enable(); + registerAISDKInstrumentation(); const channel = mockChannels.get( "orchestrion:ai:createTelemetryDispatcher", @@ -309,7 +253,7 @@ describe("AISDKPlugin", () => { onStart: existingOnStart, }; - plugin.enable(); + registerAISDKInstrumentation(); const channel = mockChannels.get( "orchestrion:ai:createTelemetryDispatcher", @@ -368,7 +312,7 @@ describe("AISDKPlugin", () => { onStart: vi.fn(), }; - plugin.enable(); + registerAISDKInstrumentation(); const channel = mockChannels.get( "orchestrion:ai:createTelemetryDispatcher", @@ -432,7 +376,7 @@ describe("AISDKPlugin", () => { onStart: existingOnStart, }; - plugin.enable(); + registerAISDKInstrumentation(); const channel = mockChannels.get( "orchestrion:ai:createTelemetryDispatcher", @@ -460,7 +404,7 @@ describe("AISDKPlugin", () => { onStart: vi.fn(), }; - plugin.enable(); + registerAISDKInstrumentation(); const channel = mockChannels.get( "orchestrion:ai:createTelemetryDispatcher", diff --git a/js/src/instrumentation/plugins/ai-sdk-plugin.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.ts similarity index 85% rename from js/src/instrumentation/plugins/ai-sdk-plugin.ts rename to js/src/instrumentation/providers/ai-sdk-instrumentation.ts index e6cbf92f4..8b797e987 100644 --- a/js/src/instrumentation/plugins/ai-sdk-plugin.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.ts @@ -1,10 +1,9 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import { debugLogger } from "../../debug-logger"; import { traceAsyncChannel, traceStreamingChannel, traceSyncStreamChannel, - unsubscribeAll, } from "../core/channel-tracing"; import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; @@ -75,7 +74,7 @@ import type { } from "../../vendor-sdk-types/ai-sdk-v7-telemetry"; import { BRAINTRUST_AI_SDK_V7_OPERATION_KEY as AI_SDK_V7_OPERATION_KEY } from "../../vendor-sdk-types/ai-sdk-v7-telemetry"; -interface AISDKPluginConfig { +interface AISDKInstrumentationConfig { /** * List of JSON paths to remove from output field. * Uses dot notation with array wildcards: "roundtrips[].request.body" @@ -150,10 +149,10 @@ const AI_SDK_V7_TELEMETRY_CALLBACKS = [ ] as const; /** - * AI SDK plugin that subscribes to instrumentation channels + * Internal AI SDK consumer that subscribes to instrumentation channels * and creates Braintrust spans. * - * This plugin handles: + * This consumer handles: * - generateText (async function) * - streamText (function returning stream) * - generateObject (async function) @@ -167,500 +166,455 @@ const AI_SDK_V7_TELEMETRY_CALLBACKS = [ * - ToolLoopAgent.stream (async method returning stream) * - WorkflowAgent.stream (async method returning stream) * - * The plugin automatically extracts: + * The consumer automatically extracts: * - Model and provider information * - Token usage metrics * - Tool calls and structured outputs * - Streaming responses with time-to-first-token */ -export class AISDKPlugin extends BasePlugin { - private config: AISDKPluginConfig; +class AISDKInstrumentationConsumer { + private config: AISDKInstrumentationConfig; - constructor(config: AISDKPluginConfig = {}) { - super(); + constructor(config: AISDKInstrumentationConfig = {}) { this.config = config; } - protected onEnable(): void { + public register(): void { this.subscribeToAISDK(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } - private subscribeToAISDK(): void { const denyOutputPaths = this.config.denyOutputPaths || DEFAULT_DENY_OUTPUT_PATHS; - this.unsubscribers.push(subscribeToAISDKV7TelemetryDispatcher()); - this.unsubscribers.push(subscribeToHarnessAgentCreateSession()); - this.unsubscribers.push( - subscribeToHarnessContinuation( - harnessAgentChannels.continueGenerate, - denyOutputPaths, - ), - subscribeToHarnessContinuation( - harnessAgentChannels.continueStream, - denyOutputPaths, - ), + subscribeToAISDKV7TelemetryDispatcher(); + subscribeToHarnessAgentCreateSession(); + subscribeToHarnessContinuation( + harnessAgentChannels.continueGenerate, + denyOutputPaths, + ); + subscribeToHarnessContinuation( + harnessAgentChannels.continueStream, + denyOutputPaths, ); // generateText - async function that may return streams - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.generateText, { - name: "generateText", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths), - extractOutput: (result, endEvent) => { - finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); - return processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ); - }, - extractMetrics: (result, _startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent), - aggregateChunks: aggregateAISDKChunks, - }), - ); + traceStreamingChannel(aiSDKChannels.generateText, { + name: "generateText", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths), + extractOutput: (result, endEvent) => { + finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); + return processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ); + }, + extractMetrics: (result, _startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent), + aggregateChunks: aggregateAISDKChunks, + }); // streamText - function returning stream - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.streamText, { - name: "streamText", - type: SpanTypeAttribute.FUNCTION, - shouldTrace: () => currentCloudflareThinkSpan() === undefined, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent, startTime), - aggregateChunks: aggregateAISDKChunks, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }), - ); + traceStreamingChannel(aiSDKChannels.streamText, { + name: "streamText", + type: SpanTypeAttribute.FUNCTION, + shouldTrace: () => currentCloudflareThinkSpan() === undefined, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths), + extractOutput: (result, endEvent) => + processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent, startTime), + aggregateChunks: aggregateAISDKChunks, + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + span, + startTime, + }), + }); // streamText - sync function returning stream (v4+, used by auto-hook) - this.unsubscribers.push( - traceSyncStreamChannel(aiSDKChannels.streamTextSync, { - name: "streamText", - type: SpanTypeAttribute.FUNCTION, - shouldTrace: () => currentCloudflareThinkSpan() === undefined, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths), - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }), - ); + traceSyncStreamChannel(aiSDKChannels.streamTextSync, { + name: "streamText", + type: SpanTypeAttribute.FUNCTION, + shouldTrace: () => currentCloudflareThinkSpan() === undefined, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths), + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + span, + startTime, + }), + }); // generateObject - async function that may return streams - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.generateObject, { - name: "generateObject", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths), - extractOutput: (result, endEvent) => { - finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); - return processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ); - }, - extractMetrics: (result, _startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent), - aggregateChunks: aggregateAISDKChunks, - }), - ); + traceStreamingChannel(aiSDKChannels.generateObject, { + name: "generateObject", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths), + extractOutput: (result, endEvent) => { + finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); + return processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ); + }, + extractMetrics: (result, _startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent), + aggregateChunks: aggregateAISDKChunks, + }); // streamObject - function returning stream - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.streamObject, { - name: "streamObject", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent, startTime), - aggregateChunks: aggregateAISDKChunks, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }), - ); + traceStreamingChannel(aiSDKChannels.streamObject, { + name: "streamObject", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths), + extractOutput: (result, endEvent) => + processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent, startTime), + aggregateChunks: aggregateAISDKChunks, + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + span, + startTime, + }), + }); // streamObject - sync function returning stream (v4+, used by auto-hook) - this.unsubscribers.push( - traceSyncStreamChannel(aiSDKChannels.streamObjectSync, { - name: "streamObject", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths), - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }), - ); + traceSyncStreamChannel(aiSDKChannels.streamObjectSync, { + name: "streamObject", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths), + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + span, + startTime, + }), + }); // embed - async embedding function - this.unsubscribers.push( - traceAsyncChannel(aiSDKChannels.embed, { - name: "embed", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event) => - prepareAISDKEmbedInput(params, event.self), - extractOutput: (result, endEvent) => - processAISDKEmbeddingOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, _startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent), - }), - ); + traceAsyncChannel(aiSDKChannels.embed, { + name: "embed", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event) => + prepareAISDKEmbedInput(params, event.self), + extractOutput: (result, endEvent) => + processAISDKEmbeddingOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, _startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent), + }); // embedMany - async embedding batch function - this.unsubscribers.push( - traceAsyncChannel(aiSDKChannels.embedMany, { - name: "embedMany", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event) => - prepareAISDKEmbedInput(params, event.self), - extractOutput: (result, endEvent) => - processAISDKEmbeddingOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, _startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent), - }), - ); + traceAsyncChannel(aiSDKChannels.embedMany, { + name: "embedMany", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event) => + prepareAISDKEmbedInput(params, event.self), + extractOutput: (result, endEvent) => + processAISDKEmbeddingOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, _startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent), + }); // rerank - async reranking function - this.unsubscribers.push( - traceAsyncChannel(aiSDKChannels.rerank, { - name: "rerank", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event) => - prepareAISDKRerankInput(params, event.self), - extractOutput: (result, endEvent) => - processAISDKRerankOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, _startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent), - }), - ); + traceAsyncChannel(aiSDKChannels.rerank, { + name: "rerank", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event) => + prepareAISDKRerankInput(params, event.self), + extractOutput: (result, endEvent) => + processAISDKRerankOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, _startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent), + }); // Agent.generate - async method - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.agentGenerate, { - name: "Agent.generate", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths, { - agentOwner: true, - }), - extractOutput: (result, endEvent) => { - finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); - return processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ); - }, - extractMetrics: (result, _startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent), - aggregateChunks: aggregateAISDKChunks, - }), - ); + traceStreamingChannel(aiSDKChannels.agentGenerate, { + name: "Agent.generate", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths, { + agentOwner: true, + }), + extractOutput: (result, endEvent) => { + finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); + return processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ); + }, + extractMetrics: (result, _startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent), + aggregateChunks: aggregateAISDKChunks, + }); // Agent.stream - async method returning stream (v5, used by wrapAISDK) - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.agentStream, { - name: "Agent.stream", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths, { - agentOwner: true, - }), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent, startTime), - aggregateChunks: aggregateAISDKChunks, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }), - ); + traceStreamingChannel(aiSDKChannels.agentStream, { + name: "Agent.stream", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths, { + agentOwner: true, + }), + extractOutput: (result, endEvent) => + processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent, startTime), + aggregateChunks: aggregateAISDKChunks, + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + span, + startTime, + }), + }); // Agent.stream - sync method returning stream (v5, used by auto-hook) - this.unsubscribers.push( - traceSyncStreamChannel(aiSDKChannels.agentStreamSync, { - name: "Agent.stream", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths, { - agentOwner: true, - }), - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }), - ); + traceSyncStreamChannel(aiSDKChannels.agentStreamSync, { + name: "Agent.stream", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths, { + agentOwner: true, + }), + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + span, + startTime, + }), + }); // HarnessAgent.generate - one task span per agent turn - this.unsubscribers.push( - traceStreamingChannel(harnessAgentChannels.generate, { - name: "HarnessAgent.generate", - startSpan: _internalStartSpanWithInitialMerge, - type: SpanTypeAttribute.TASK, - extractInput: ([params], event, span) => - prepareAISDKHarnessAgentInput(params, event.self, span), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result) => extractTokenMetrics(result), - aggregateChunks: aggregateAISDKChunks, - }), - ); + traceStreamingChannel(harnessAgentChannels.generate, { + name: "HarnessAgent.generate", + startSpan: _internalStartSpanWithInitialMerge, + type: SpanTypeAttribute.TASK, + extractInput: ([params], event, span) => + prepareAISDKHarnessAgentInput(params, event.self, span), + extractOutput: (result, endEvent) => + processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result) => extractTokenMetrics(result), + aggregateChunks: aggregateAISDKChunks, + }); // HarnessAgent.stream - async method returning an AI SDK stream result - this.unsubscribers.push( - traceStreamingChannel(harnessAgentChannels.stream, { - name: "HarnessAgent.stream", - startSpan: _internalStartSpanWithInitialMerge, - type: SpanTypeAttribute.TASK, - extractInput: ([params], event, span) => - prepareAISDKHarnessAgentInput(params, event.self, span), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, startTime) => ({ - ...extractTokenMetrics(result), - ...(startTime === undefined - ? {} - : { - time_to_first_token: getCurrentUnixTimestamp() - startTime, - }), - }), - aggregateChunks: aggregateAISDKChunks, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - resolvePromiseUsage: true, - span, - startTime, - }), + traceStreamingChannel(harnessAgentChannels.stream, { + name: "HarnessAgent.stream", + startSpan: _internalStartSpanWithInitialMerge, + type: SpanTypeAttribute.TASK, + extractInput: ([params], event, span) => + prepareAISDKHarnessAgentInput(params, event.self, span), + extractOutput: (result, endEvent) => + processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, startTime) => ({ + ...extractTokenMetrics(result), + ...(startTime === undefined + ? {} + : { + time_to_first_token: getCurrentUnixTimestamp() - startTime, + }), }), - ); + aggregateChunks: aggregateAISDKChunks, + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + resolvePromiseUsage: true, + span, + startTime, + }), + }); // Trace a continuation as its own task only when its original turn cannot // be recovered. Known continuations extend the original Harness task. - this.unsubscribers.push( - traceStreamingChannel(harnessAgentChannels.continueGenerate, { - name: "HarnessAgent.continueGenerate", - shouldTrace: (args) => - !harnessContinuationParent(harnessSessionFromArguments(args)), - startSpan: _internalStartSpanWithInitialMerge, - type: SpanTypeAttribute.TASK, - extractInput: ([params], event, span) => - prepareAISDKHarnessAgentInput(params, event.self, span), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result) => extractTokenMetrics(result), - aggregateChunks: aggregateAISDKChunks, - }), - ); + traceStreamingChannel(harnessAgentChannels.continueGenerate, { + name: "HarnessAgent.continueGenerate", + shouldTrace: (args) => + !harnessContinuationParent(harnessSessionFromArguments(args)), + startSpan: _internalStartSpanWithInitialMerge, + type: SpanTypeAttribute.TASK, + extractInput: ([params], event, span) => + prepareAISDKHarnessAgentInput(params, event.self, span), + extractOutput: (result, endEvent) => + processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result) => extractTokenMetrics(result), + aggregateChunks: aggregateAISDKChunks, + }); - this.unsubscribers.push( - traceStreamingChannel(harnessAgentChannels.continueStream, { - name: "HarnessAgent.continueStream", - shouldTrace: (args) => - !harnessContinuationParent(harnessSessionFromArguments(args)), - startSpan: _internalStartSpanWithInitialMerge, - type: SpanTypeAttribute.TASK, - extractInput: ([params], event, span) => - prepareAISDKHarnessAgentInput(params, event.self, span), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, startTime) => ({ - ...extractTokenMetrics(result), - ...(startTime === undefined - ? {} - : { - time_to_first_token: getCurrentUnixTimestamp() - startTime, - }), - }), - aggregateChunks: aggregateAISDKChunks, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - resolvePromiseUsage: true, - span, - startTime, - }), + traceStreamingChannel(harnessAgentChannels.continueStream, { + name: "HarnessAgent.continueStream", + shouldTrace: (args) => + !harnessContinuationParent(harnessSessionFromArguments(args)), + startSpan: _internalStartSpanWithInitialMerge, + type: SpanTypeAttribute.TASK, + extractInput: ([params], event, span) => + prepareAISDKHarnessAgentInput(params, event.self, span), + extractOutput: (result, endEvent) => + processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, startTime) => ({ + ...extractTokenMetrics(result), + ...(startTime === undefined + ? {} + : { + time_to_first_token: getCurrentUnixTimestamp() - startTime, + }), }), - ); + aggregateChunks: aggregateAISDKChunks, + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + resolvePromiseUsage: true, + span, + startTime, + }), + }); // ToolLoopAgent.generate - async method - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.toolLoopAgentGenerate, { - name: "ToolLoopAgent.generate", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths, { - agentOwner: true, - }), - extractOutput: (result, endEvent) => { - finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); - return processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ); - }, - extractMetrics: (result, _startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent), - aggregateChunks: aggregateAISDKChunks, - }), - ); + traceStreamingChannel(aiSDKChannels.toolLoopAgentGenerate, { + name: "ToolLoopAgent.generate", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths, { + agentOwner: true, + }), + extractOutput: (result, endEvent) => { + finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); + return processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ); + }, + extractMetrics: (result, _startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent), + aggregateChunks: aggregateAISDKChunks, + }); // ToolLoopAgent.stream - async method returning stream - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.toolLoopAgentStream, { - name: "ToolLoopAgent.stream", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths, { - agentOwner: true, - }), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent, startTime), - aggregateChunks: aggregateAISDKChunks, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }), - ); + traceStreamingChannel(aiSDKChannels.toolLoopAgentStream, { + name: "ToolLoopAgent.stream", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKCallInput(params, event, span, denyOutputPaths, { + agentOwner: true, + }), + extractOutput: (result, endEvent) => + processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ), + extractMetrics: (result, startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent, startTime), + aggregateChunks: aggregateAISDKChunks, + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + span, + startTime, + }), + }); // WorkflowAgent.stream - async method returning stream - this.unsubscribers.push( - traceStreamingChannel(aiSDKChannels.workflowAgentStream, { - name: "WorkflowAgent.stream", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKWorkflowAgentStreamInput( - params, - event, - span, - denyOutputPaths, - ), - extractOutput: (result, endEvent) => { - finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); - return processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ); - }, - extractMetrics: (result, _startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent), - aggregateChunks: aggregateAISDKChunks, - onComplete: ({ span }) => { - unregisterWorkflowAgentWrapperSpan(span); - }, - onError: ({ event, span }) => { - finalizeAISDKChildTracing(event as { [key: string]: unknown }); - unregisterWorkflowAgentWrapperSpan(span); - }, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - onComplete: () => unregisterWorkflowAgentWrapperSpan(span), - onCancel: () => unregisterWorkflowAgentWrapperSpan(span), - onError: () => unregisterWorkflowAgentWrapperSpan(span), - result, - span, - startTime, - }), - }), - ); + traceStreamingChannel(aiSDKChannels.workflowAgentStream, { + name: "WorkflowAgent.stream", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params], event, span) => + prepareAISDKWorkflowAgentStreamInput( + params, + event, + span, + denyOutputPaths, + ), + extractOutput: (result, endEvent) => { + finalizeAISDKChildTracing(endEvent as { [key: string]: unknown }); + return processAISDKOutput( + result, + resolveDenyOutputPaths(endEvent, denyOutputPaths), + ); + }, + extractMetrics: (result, _startTime, endEvent) => + extractTopLevelAISDKMetrics(result, endEvent), + aggregateChunks: aggregateAISDKChunks, + onComplete: ({ span }) => { + unregisterWorkflowAgentWrapperSpan(span); + }, + onError: ({ event, span }) => { + finalizeAISDKChildTracing(event as { [key: string]: unknown }); + unregisterWorkflowAgentWrapperSpan(span); + }, + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + onComplete: () => unregisterWorkflowAgentWrapperSpan(span), + onCancel: () => unregisterWorkflowAgentWrapperSpan(span), + onError: () => unregisterWorkflowAgentWrapperSpan(span), + result, + span, + startTime, + }), + }); } } -function subscribeToHarnessAgentCreateSession(): () => void { +function subscribeToHarnessAgentCreateSession(): void { const channel = harnessAgentChannels.createSession.tracingChannel(); const parents = new WeakMap(); const handlers: IsoChannelHandlers< @@ -682,7 +636,6 @@ function subscribeToHarnessAgentCreateSession(): () => void { }; channel.subscribe(handlers); - return () => channel.unsubscribe(handlers); } type HarnessContinuationChannel = typeof harnessAgentChannels.continueGenerate; @@ -700,14 +653,11 @@ function harnessContinuationParentFromEvent( function subscribeToHarnessContinuation( continuationChannel: HarnessContinuationChannel, defaultDenyOutputPaths: string[], -): () => void { +): void { const channel = continuationChannel.tracingChannel(); const parents = new WeakMap(); const startTimes = new WeakMap(); - const unbindParentStore = bindHarnessTurnParentToStart( - channel, - harnessContinuationParentFromEvent, - ); + bindHarnessTurnParentToStart(channel, harnessContinuationParentFromEvent); const handlers: IsoChannelHandlers< ChannelMessage > = { @@ -833,13 +783,9 @@ function subscribeToHarnessContinuation( }; channel.subscribe(handlers); - return () => { - unbindParentStore(); - channel.unsubscribe(handlers); - }; } -function subscribeToAISDKV7TelemetryDispatcher(): () => void { +function subscribeToAISDKV7TelemetryDispatcher(): void { const channel = aiSDKChannels.v7CreateTelemetryDispatcher.tracingChannel(); const telemetry = braintrustAISDKTelemetry(); const handlers: IsoChannelHandlers< @@ -860,10 +806,6 @@ function subscribeToAISDKV7TelemetryDispatcher(): () => void { }; channel.subscribe(handlers); - - return () => { - channel.unsubscribe(handlers); - }; } function patchAISDKV7TelemetryDispatcher( @@ -4274,3 +4216,12 @@ function omit( return result; } + +let aiSDKInstrumentationConsumer: AISDKInstrumentationConsumer | undefined; + +export function registerAISDKInstrumentation( + config: AISDKInstrumentationConfig = {}, +): void { + aiSDKInstrumentationConsumer ??= new AISDKInstrumentationConsumer(config); + aiSDKInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/ai-sdk-v7-telemetry.test.ts b/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts similarity index 100% rename from js/src/instrumentation/plugins/ai-sdk-v7-telemetry.test.ts rename to js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts diff --git a/js/src/instrumentation/plugins/anthropic-channels.ts b/js/src/instrumentation/providers/anthropic-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/anthropic-channels.ts rename to js/src/instrumentation/providers/anthropic-channels.ts diff --git a/js/src/instrumentation/plugins/anthropic-plugin.test.ts b/js/src/instrumentation/providers/anthropic-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/anthropic-plugin.test.ts rename to js/src/instrumentation/providers/anthropic-instrumentation.test.ts index ffad58aff..3e3d56708 100644 --- a/js/src/instrumentation/plugins/anthropic-plugin.test.ts +++ b/js/src/instrumentation/providers/anthropic-instrumentation.test.ts @@ -12,7 +12,7 @@ import { aggregateAnthropicStreamChunks, processAttachmentsInInput, coalesceInput, -} from "./anthropic-plugin"; +} from "./anthropic-instrumentation"; import { Attachment } from "../../logger"; const parseMetricsFromUsageForTest = (usage: unknown) => diff --git a/js/src/instrumentation/plugins/anthropic-plugin.ts b/js/src/instrumentation/providers/anthropic-instrumentation.ts similarity index 98% rename from js/src/instrumentation/plugins/anthropic-plugin.ts rename to js/src/instrumentation/providers/anthropic-instrumentation.ts index 36cad5a5c..9b6cc128e 100644 --- a/js/src/instrumentation/plugins/anthropic-plugin.ts +++ b/js/src/instrumentation/providers/anthropic-instrumentation.ts @@ -1,5 +1,5 @@ -import { BasePlugin, toLoggedError } from "../core"; -import { traceStreamingChannel, unsubscribeAll } from "../core/channel-tracing"; +import { toLoggedError } from "../core"; +import { traceStreamingChannel } from "../core/channel-tracing"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import { Attachment, @@ -93,30 +93,26 @@ const ANTHROPIC_TOOL_RUNNER_TOOL_WRAPPED = Symbol.for( ); /** - * Auto-instrumentation plugin for the Anthropic SDK. + * Internal auto-instrumentation consumer for the Anthropic SDK. * - * This plugin subscribes to orchestrion channels for Anthropic SDK methods + * This consumer subscribes to orchestrion channels for Anthropic SDK methods * and creates Braintrust spans to track: * - messages.create (streaming and non-streaming) * - beta.messages.create (streaming and non-streaming) * - * The plugin handles: + * The consumer handles: * - Anthropic-specific token metrics (including cache tokens) * - Processing message streams * - Converting base64 attachments to Attachment objects * - Streaming and non-streaming responses */ -export class AnthropicPlugin extends BasePlugin { - protected onEnable(): void { +class AnthropicInstrumentationConsumer { + public register(): void { this.subscribeToAnthropicChannels(); this.subscribeToAnthropicToolRunner(); this.subscribeToAnthropicSessionStreams(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } - private subscribeToAnthropicChannels(): void { const anthropicConfig = { name: "anthropic.messages.create", @@ -163,17 +159,13 @@ export class AnthropicPlugin extends BasePlugin { }; // Messages API - supports streaming via stream=true parameter - this.unsubscribers.push( - traceStreamingChannel(anthropicChannels.messagesCreate, anthropicConfig), - ); + traceStreamingChannel(anthropicChannels.messagesCreate, anthropicConfig); // Beta Messages API - supports streaming via stream=true parameter - this.unsubscribers.push( - traceStreamingChannel(anthropicChannels.betaMessagesCreate, { - ...anthropicConfig, - name: "anthropic.messages.create", - }), - ); + traceStreamingChannel(anthropicChannels.betaMessagesCreate, { + ...anthropicConfig, + name: "anthropic.messages.create", + }); } private subscribeToAnthropicToolRunner(): void { @@ -250,9 +242,6 @@ export class AnthropicPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - tracingChannel.unsubscribe(handlers); - }); } private subscribeToAnthropicSessionStreams(): void { @@ -305,7 +294,6 @@ export class AnthropicPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => tracingChannel.unsubscribe(handlers)); } } @@ -1686,3 +1674,12 @@ export function coalesceInput( } return input; } + +let anthropicInstrumentationConsumer: + | AnthropicInstrumentationConsumer + | undefined; + +export function registerAnthropicInstrumentation(): void { + anthropicInstrumentationConsumer ??= new AnthropicInstrumentationConsumer(); + anthropicInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/anthropic-sessions-plugin.test.ts b/js/src/instrumentation/providers/anthropic-sessions-instrumentation.test.ts similarity index 97% rename from js/src/instrumentation/plugins/anthropic-sessions-plugin.test.ts rename to js/src/instrumentation/providers/anthropic-sessions-instrumentation.test.ts index 1d4cb48ac..44bd501d6 100644 --- a/js/src/instrumentation/plugins/anthropic-sessions-plugin.test.ts +++ b/js/src/instrumentation/providers/anthropic-sessions-instrumentation.test.ts @@ -28,11 +28,11 @@ vi.mock("../../logger", async (importOriginal) => { import iso from "../../isomorph"; import { collectAnthropicSession } from "../../wrappers/anthropic-session-collector"; -import { AnthropicPlugin } from "./anthropic-plugin"; +import { registerAnthropicInstrumentation } from "./anthropic-instrumentation"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; -describe("AnthropicPlugin Sessions instrumentation", () => { +describe("registerAnthropicInstrumentation Sessions instrumentation", () => { let currentSpan: TestSpan | undefined; let handlersByName: Map; let spans: TestSpan[]; @@ -43,7 +43,6 @@ describe("AnthropicPlugin Sessions instrumentation", () => { spans = []; mockNewTracingChannel.mockImplementation((name: string) => ({ subscribe: vi.fn((handlers) => handlersByName.set(name, handlers)), - unsubscribe: vi.fn(), })); mockWithCurrent.mockImplementation( (span: TestSpan, callback: () => unknown) => { @@ -78,7 +77,7 @@ describe("AnthropicPlugin Sessions instrumentation", () => { }); it("records a session turn with model and tool children", async () => { - new AnthropicPlugin().enable(); + registerAnthropicInstrumentation(); const handlers = handlersByName.get( "orchestrion:@anthropic-ai/sdk:beta.sessions.events.stream", ); @@ -262,7 +261,7 @@ describe("AnthropicPlugin Sessions instrumentation", () => { }); it("does not record an uncollected session stream", async () => { - new AnthropicPlugin().enable(); + registerAnthropicInstrumentation(); const handlers = handlersByName.get( "orchestrion:@anthropic-ai/sdk:beta.sessions.events.stream", ); @@ -286,7 +285,7 @@ describe("AnthropicPlugin Sessions instrumentation", () => { }); it("uses a distinct task name for thread streams and records denials", async () => { - new AnthropicPlugin().enable(); + registerAnthropicInstrumentation(); const handlers = handlersByName.get( "orchestrion:@anthropic-ai/sdk:beta.sessions.threads.events.stream", ); diff --git a/js/src/instrumentation/plugins/bedrock-runtime-channels.ts b/js/src/instrumentation/providers/bedrock-runtime-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/bedrock-runtime-channels.ts rename to js/src/instrumentation/providers/bedrock-runtime-channels.ts diff --git a/js/src/instrumentation/plugins/bedrock-runtime-common.ts b/js/src/instrumentation/providers/bedrock-runtime-common.ts similarity index 100% rename from js/src/instrumentation/plugins/bedrock-runtime-common.ts rename to js/src/instrumentation/providers/bedrock-runtime-common.ts diff --git a/js/src/instrumentation/plugins/bedrock-runtime-plugin.test.ts b/js/src/instrumentation/providers/bedrock-runtime-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/bedrock-runtime-plugin.test.ts rename to js/src/instrumentation/providers/bedrock-runtime-instrumentation.test.ts index fc74a9068..b22906083 100644 --- a/js/src/instrumentation/plugins/bedrock-runtime-plugin.test.ts +++ b/js/src/instrumentation/providers/bedrock-runtime-instrumentation.test.ts @@ -9,7 +9,7 @@ import { aggregateBedrockConverseStreamChunks, aggregateInvokeModelResponseStreamChunks, parseBedrockRuntimeMetrics, -} from "./bedrock-runtime-plugin"; +} from "./bedrock-runtime-instrumentation"; try { configureNode(); @@ -29,7 +29,7 @@ class GetObjectCommand { constructor(public input: Record) {} } -describe("BedrockRuntimePlugin", () => { +describe("registerBedrockRuntimeInstrumentation", () => { let backgroundLogger: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; @@ -41,7 +41,7 @@ describe("BedrockRuntimePlugin", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "bedrock-runtime-plugin.test.ts", + projectName: "bedrock-runtime-instrumentation.test.ts", projectId: "test-project-id", }); }); diff --git a/js/src/instrumentation/plugins/bedrock-runtime-plugin.ts b/js/src/instrumentation/providers/bedrock-runtime-instrumentation.ts similarity index 97% rename from js/src/instrumentation/plugins/bedrock-runtime-plugin.ts rename to js/src/instrumentation/providers/bedrock-runtime-instrumentation.ts index a759d1ffe..dfd5a47be 100644 --- a/js/src/instrumentation/plugins/bedrock-runtime-plugin.ts +++ b/js/src/instrumentation/providers/bedrock-runtime-instrumentation.ts @@ -1,5 +1,4 @@ -import { BasePlugin } from "../core"; -import { traceStreamingChannel, unsubscribeAll } from "../core/channel-tracing"; +import { traceStreamingChannel } from "../core/channel-tracing"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import { SpanTypeAttribute, isObject } from "../../../util/index"; import { getCurrentUnixTimestamp } from "../../util"; @@ -25,26 +24,18 @@ import { getBedrockRuntimeOperation, } from "./bedrock-runtime-common"; -export class BedrockRuntimePlugin extends BasePlugin { - protected onEnable(): void { - this.unsubscribers.push( - ...[ - bedrockRuntimeChannels.clientSend, - smithyCoreChannels.clientSend, - smithyClientChannels.clientSend, - ].map((channel) => traceBedrockRuntimeClientSendChannel(channel)), - ); - } - - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); +export function registerBedrockRuntimeInstrumentation(): void { + for (const channel of [ + bedrockRuntimeChannels.clientSend, + smithyCoreChannels.clientSend, + smithyClientChannels.clientSend, + ]) { + traceBedrockRuntimeClientSendChannel(channel); } } -function traceBedrockRuntimeClientSendChannel( - channel: AnyAsyncChannel, -): () => void { - return traceStreamingChannel(channel, { +function traceBedrockRuntimeClientSendChannel(channel: AnyAsyncChannel): void { + traceStreamingChannel(channel, { name: ([command]) => buildBedrockRuntimeSpanInfo(command).name, shouldTrace: ([command, optionsOrCb, cb]) => getBedrockRuntimeOperation(command) !== undefined && diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-channels.ts b/js/src/instrumentation/providers/claude-agent-sdk-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/claude-agent-sdk-channels.ts rename to js/src/instrumentation/providers/claude-agent-sdk-channels.ts diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-instrumentation-constants.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation-constants.ts similarity index 100% rename from js/src/instrumentation/plugins/claude-agent-sdk-instrumentation-constants.ts rename to js/src/instrumentation/providers/claude-agent-sdk-instrumentation-constants.ts diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-plugin.streaming.test.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.streaming.test.ts similarity index 99% rename from js/src/instrumentation/plugins/claude-agent-sdk-plugin.streaming.test.ts rename to js/src/instrumentation/providers/claude-agent-sdk-instrumentation.streaming.test.ts index 42101e066..ddaeeab1f 100644 --- a/js/src/instrumentation/plugins/claude-agent-sdk-plugin.streaming.test.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.streaming.test.ts @@ -120,7 +120,7 @@ describe("Claude Agent SDK streaming instrumentation", () => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ projectId: "test-project-id", - projectName: "claude-agent-sdk-plugin.streaming.test.ts", + projectName: "claude-agent-sdk-instrumentation.streaming.test.ts", }); }); diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-plugin.test.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts similarity index 86% rename from js/src/instrumentation/plugins/claude-agent-sdk-plugin.test.ts rename to js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts index f1d446bcd..93265e4a3 100644 --- a/js/src/instrumentation/plugins/claude-agent-sdk-plugin.test.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts @@ -30,7 +30,7 @@ vi.mock("../core/stream-patcher", () => ({ }), })); -import { ClaudeAgentSDKPlugin } from "./claude-agent-sdk-plugin"; +import { registerClaudeAgentSDKInstrumentation } from "./claude-agent-sdk-instrumentation"; import iso from "../../isomorph"; import { startSpan } from "../../logger"; @@ -72,79 +72,26 @@ vi.mock("../../wrappers/attachment-utils", () => ({ // rules, so these tests run the real implementation rather than a stand-in that // could drift from it. -vi.mock("../core", async (importOriginal) => { - const actual = await importOriginal(); - - return { - ...actual, - BasePlugin: class BasePlugin { - protected enabled = false; - protected unsubscribers: Array<() => void> = []; - - enable(): void { - if (this.enabled) { - return; - } - this.enabled = true; - this.onEnable(); - } - - disable(): void { - if (!this.enabled) { - return; - } - this.enabled = false; - this.onDisable(); - } - - protected onEnable(): void { - // To be implemented by subclass - } - - protected onDisable(): void { - // To be implemented by subclass - } - }, - isAsyncIterable: vi.fn( - (val: unknown) => - val !== null && - typeof val === "object" && - Symbol.asyncIterator in val && - typeof (val as any)[Symbol.asyncIterator] === "function", - ), - patchStreamIfNeeded: vi.fn((stream, _callbacks) => { - // Return the stream unchanged for simple tests - return stream; - }), - }; -}); - -describe("ClaudeAgentSDKPlugin", () => { - let plugin: ClaudeAgentSDKPlugin; +describe("registerClaudeAgentSDKInstrumentation", () => { let mockChannel: any; - let mockUnsubscribe: any; beforeEach(() => { streamPatcherMock.options = undefined; - mockUnsubscribe = vi.fn(); mockChannel = { subscribe: vi.fn(), - unsubscribe: mockUnsubscribe, hasSubscribers: false, }; mockNewTracingChannel.mockReturnValue(mockChannel); - - plugin = new ClaudeAgentSDKPlugin(); }); afterEach(() => { vi.clearAllMocks(); }); - describe("enable", () => { - it("should enable the plugin and subscribe to channels", () => { - plugin.enable(); + describe("registration", () => { + it("subscribes to the channel", () => { + registerClaudeAgentSDKInstrumentation(); expect(mockNewTracingChannel).toHaveBeenCalledWith( "orchestrion:@anthropic-ai/claude-agent-sdk:query", @@ -158,50 +105,13 @@ describe("ClaudeAgentSDKPlugin", () => { }), ); }); - - it("should not subscribe twice if already enabled", () => { - plugin.enable(); - plugin.enable(); - - expect(mockChannel.subscribe).toHaveBeenCalledTimes(1); - }); - - it("should store unsubscribe function", () => { - plugin.enable(); - - expect((plugin as any).unsubscribers).toHaveLength(1); - expect((plugin as any).unsubscribers[0]).toBeInstanceOf(Function); - }); - }); - - describe("disable", () => { - it("should unsubscribe from all channels", () => { - plugin.enable(); - plugin.disable(); - - expect(mockUnsubscribe).toHaveBeenCalledTimes(1); - expect((plugin as any).unsubscribers).toHaveLength(0); - }); - - it("should not unsubscribe if not enabled", () => { - plugin.disable(); - - expect(mockUnsubscribe).not.toHaveBeenCalled(); - }); - - it("should clear unsubscribers array", () => { - plugin.enable(); - plugin.disable(); - - expect((plugin as any).unsubscribers).toHaveLength(0); - }); }); describe("channel subscription handlers", () => { let handlers: any; beforeEach(() => { - plugin.enable(); + registerClaudeAgentSDKInstrumentation(); handlers = mockChannel.subscribe.mock.calls[0][0]; }); @@ -745,24 +655,4 @@ describe("ClaudeAgentSDKPlugin", () => { }); }); }); - - describe("enable/disable lifecycle", () => { - it("should allow re-enabling after disable", () => { - plugin.enable(); - plugin.disable(); - plugin.enable(); - - expect(mockChannel.subscribe).toHaveBeenCalledTimes(2); - }); - - it("should properly clean up on multiple enable/disable cycles", () => { - plugin.enable(); - plugin.disable(); - plugin.enable(); - plugin.disable(); - - expect(mockUnsubscribe).toHaveBeenCalledTimes(2); - expect((plugin as any).unsubscribers).toHaveLength(0); - }); - }); }); diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-plugin.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts similarity index 99% rename from js/src/instrumentation/plugins/claude-agent-sdk-plugin.ts rename to js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts index 96f52c9b6..9944ebe60 100644 --- a/js/src/instrumentation/plugins/claude-agent-sdk-plugin.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts @@ -1,4 +1,3 @@ -import { BasePlugin } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { IsoChannelHandlers } from "../../isomorph"; @@ -1578,18 +1577,11 @@ async function finalizeQuerySpan(state: QueryState): Promise { } } -export class ClaudeAgentSDKPlugin extends BasePlugin { - protected onEnable(): void { +class ClaudeAgentSDKInstrumentationConsumer { + public register(): void { this.subscribeToQuery(); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - } - private subscribeToQuery(): void { const channel = claudeAgentSDKChannels.query.tracingChannel(); const spans = new WeakMap(); @@ -1854,8 +1846,15 @@ export class ClaudeAgentSDKPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => { - channel.unsubscribe(handlers); - }); } } + +let claudeAgentSDKInstrumentationConsumer: + | ClaudeAgentSDKInstrumentationConsumer + | undefined; + +export function registerClaudeAgentSDKInstrumentation(): void { + claudeAgentSDKInstrumentationConsumer ??= + new ClaudeAgentSDKInstrumentationConsumer(); + claudeAgentSDKInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-local-tool-context.ts b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-context.ts similarity index 100% rename from js/src/instrumentation/plugins/claude-agent-sdk-local-tool-context.ts rename to js/src/instrumentation/providers/claude-agent-sdk-local-tool-context.ts diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-local-tool-spans.ts b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts similarity index 100% rename from js/src/instrumentation/plugins/claude-agent-sdk-local-tool-spans.ts rename to js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts diff --git a/js/src/instrumentation/plugins/cloudflare-agents-channels.ts b/js/src/instrumentation/providers/cloudflare-agents-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/cloudflare-agents-channels.ts rename to js/src/instrumentation/providers/cloudflare-agents-channels.ts diff --git a/js/src/instrumentation/plugins/cloudflare-agents-plugin.test.ts b/js/src/instrumentation/providers/cloudflare-agents-instrumentation.test.ts similarity index 88% rename from js/src/instrumentation/plugins/cloudflare-agents-plugin.test.ts rename to js/src/instrumentation/providers/cloudflare-agents-instrumentation.test.ts index d6d2de7de..970297b01 100644 --- a/js/src/instrumentation/plugins/cloudflare-agents-plugin.test.ts +++ b/js/src/instrumentation/providers/cloudflare-agents-instrumentation.test.ts @@ -29,14 +29,13 @@ vi.mock("../../isomorph", () => ({ })); import iso from "../../isomorph"; -import { CloudflareAgentsPlugin } from "./cloudflare-agents-plugin"; +import { registerCloudflareAgentsInstrumentation } from "./cloudflare-agents-instrumentation"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; -describe("CloudflareAgentsPlugin", () => { +describe("registerCloudflareAgentsInstrumentation", () => { let handlers: any; let subscribe: ReturnType; - let unsubscribe: ReturnType; let spans: Array<{ args: any; context: any; @@ -49,8 +48,7 @@ describe("CloudflareAgentsPlugin", () => { subscribe = vi.fn((nextHandlers) => { handlers = nextHandlers; }); - unsubscribe = vi.fn(); - mockNewTracingChannel.mockReturnValue({ subscribe, unsubscribe }); + mockNewTracingChannel.mockReturnValue({ subscribe }); mockStartSpan.mockImplementation((args: any, context: any) => { const span = { args, context, end: vi.fn(), log: vi.fn() }; spans.push(span); @@ -62,19 +60,13 @@ describe("CloudflareAgentsPlugin", () => { vi.clearAllMocks(); }); - it("subscribes idempotently to Agent.runAgentTool", () => { - const plugin = new CloudflareAgentsPlugin(); - plugin.enable(); - plugin.enable(); + it("subscribes to the process-lifetime channel", () => { + registerCloudflareAgentsInstrumentation(); expect(mockNewTracingChannel).toHaveBeenCalledWith( "orchestrion:agents:Agent.runAgentTool", ); expect(subscribe).toHaveBeenCalledTimes(1); - - plugin.disable(); - plugin.disable(); - expect(unsubscribe).toHaveBeenCalledTimes(1); }); it("keeps SDK-controlled context out of the public start-span arguments", () => { @@ -86,7 +78,7 @@ describe("CloudflareAgentsPlugin", () => { }); it("records only the child class name, input, and completed output", () => { - new CloudflareAgentsPlugin().enable(); + registerCloudflareAgentsInstrumentation(); class ResearchAgent {} const event = { arguments: [ @@ -142,7 +134,7 @@ describe("CloudflareAgentsPlugin", () => { }); it("records returned terminal error strings", () => { - new CloudflareAgentsPlugin().enable(); + registerCloudflareAgentsInstrumentation(); class FailingAgent {} const event = { arguments: [FailingAgent, { input: "fail" }] }; @@ -164,7 +156,7 @@ describe("CloudflareAgentsPlugin", () => { }); it("records the original rejection and preserves concurrent span state", () => { - new CloudflareAgentsPlugin().enable(); + registerCloudflareAgentsInstrumentation(); class FirstAgent {} class SecondAgent {} const first = { arguments: [FirstAgent, { input: 1 }] }; @@ -188,7 +180,7 @@ describe("CloudflareAgentsPlugin", () => { }); it("skips detached runs and does not invoke getters", () => { - new CloudflareAgentsPlugin().enable(); + registerCloudflareAgentsInstrumentation(); const nameGetter = vi.fn(() => "GetterAgent"); const inputGetter = vi.fn(() => "getter-input"); const AgentWithGetter = Object.defineProperty(function () {}, "name", { diff --git a/js/src/instrumentation/providers/cloudflare-agents-instrumentation.ts b/js/src/instrumentation/providers/cloudflare-agents-instrumentation.ts new file mode 100644 index 000000000..36fe3671a --- /dev/null +++ b/js/src/instrumentation/providers/cloudflare-agents-instrumentation.ts @@ -0,0 +1,129 @@ +import { debugLogger } from "../../debug-logger"; +import type { IsoChannelHandlers } from "../../isomorph"; +import { _internalStartSpanWithContext } from "../../logger"; +import type { Span } from "../../logger"; +import { + INSTRUMENTATION_NAMES, + withSpanInstrumentationName, +} from "../../span-origin"; +import { SpanTypeAttribute } from "../../../util/index"; +import type { ChannelMessage } from "../core/channel-definitions"; +import { cloudflareAgentsChannels } from "./cloudflare-agents-channels"; + +const CLOUDFLARE_WORKERS_CONTEXT = { + span_origin: { + environment: { type: "server", name: "cloudflare_workers" }, + }, +}; + +export function registerCloudflareAgentsInstrumentation(): void { + const channel = cloudflareAgentsChannels.runAgentTool.tracingChannel(); + const spans = new WeakMap(); + const handlers: IsoChannelHandlers< + ChannelMessage + > = { + start: (event) => { + try { + const agentClass = event.arguments[0]; + const options = event.arguments[1]; + if (ownValue(options, "detached")) { + return; + } + + const name = ownValue(agentClass, "name"); + if (typeof name !== "string" || name.length === 0) { + debugLogger.warn( + "Skipping Cloudflare Agents runAgentTool span because the child agent class has no name.", + ); + return; + } + + const span = _internalStartSpanWithContext( + withSpanInstrumentationName( + { + name, + spanAttributes: { type: SpanTypeAttribute.TOOL }, + event: { + input: ownValue(options, "input"), + }, + }, + INSTRUMENTATION_NAMES.CLOUDFLARE_AGENTS, + ), + CLOUDFLARE_WORKERS_CONTEXT, + ); + spans.set(event, span); + } catch (error) { + logInstrumentationError("start", error); + } + }, + asyncEnd: (event) => { + const span = spans.get(event); + if (!span) { + return; + } + spans.delete(event); + + try { + const status = ownValue(event.result, "status"); + if (status === "completed") { + span.log({ output: ownValue(event.result, "output") }); + } else { + const error = ownValue(event.result, "error"); + if (typeof error === "string") { + span.log({ error }); + } + } + } catch (error) { + logInstrumentationError("completion", error); + } finally { + safelyEndSpan(span); + } + }, + error: (event) => { + const span = spans.get(event); + if (!span) { + return; + } + spans.delete(event); + + try { + span.log({ error: event.error }); + } catch (error) { + logInstrumentationError("rejection", error); + } finally { + safelyEndSpan(span); + } + }, + }; + + channel.subscribe(handlers); +} + +function ownValue(value: unknown, key: PropertyKey): unknown { + if (!isObjectLike(value)) { + return undefined; + } + const descriptor = Object.getOwnPropertyDescriptor(value, key); + return descriptor && "value" in descriptor ? descriptor.value : undefined; +} + +function isObjectLike(value: unknown): value is object { + return ( + (typeof value === "object" && value !== null) || typeof value === "function" + ); +} + +function safelyEndSpan(span: Span): void { + try { + span.end(); + } catch (error) { + logInstrumentationError("span end", error); + } +} + +function logInstrumentationError(operation: string, error: unknown): void { + debugLogger.error( + `Failed to process Cloudflare Agents ${operation} instrumentation:`, + error, + ); +} diff --git a/js/src/instrumentation/plugins/cloudflare-ai-chat-channels.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/cloudflare-ai-chat-channels.ts rename to js/src/instrumentation/providers/cloudflare-ai-chat-channels.ts diff --git a/js/src/instrumentation/plugins/cloudflare-ai-chat-plugin.test.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts similarity index 94% rename from js/src/instrumentation/plugins/cloudflare-ai-chat-plugin.test.ts rename to js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts index d20631a2d..860ab0b18 100644 --- a/js/src/instrumentation/plugins/cloudflare-ai-chat-plugin.test.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts @@ -30,12 +30,11 @@ import { INSTRUMENTATION_NAMES, INTERNAL_SPAN_INSTRUMENTATION_NAME, } from "../../span-origin"; -import { CloudflareAIChatPlugin } from "./cloudflare-ai-chat-plugin"; +import { registerCloudflareAIChatInstrumentation } from "./cloudflare-ai-chat-consumer"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; -describe("CloudflareAIChatPlugin", () => { - let plugin: CloudflareAIChatPlugin; +describe("registerCloudflareAIChatInstrumentation", () => { let channels: Map>; beforeEach(() => { @@ -54,16 +53,14 @@ describe("CloudflareAIChatPlugin", () => { log: vi.fn(), })); mockInternalGetGlobalState.mockReturnValue(undefined); - plugin = new CloudflareAIChatPlugin(); }); afterEach(() => { - plugin.disable(); vi.clearAllMocks(); }); it("captures the full successful turn and binds queued work", async () => { - plugin.enable(); + registerCloudflareAIChatInstrumentation(); const turnHandlers = turnChannel().handlers(); const callback = vi.fn(async () => "callback-result"); const agent = { @@ -132,7 +129,7 @@ describe("CloudflareAIChatPlugin", () => { }); it("correlates response errors and preserves partial output", async () => { - plugin.enable(); + registerCloudflareAIChatInstrumentation(); const turnHandlers = turnChannel().handlers(); const responseHandlers = responseChannel().handlers(); const agent = { messages: [], onChatResponse() {} }; @@ -169,7 +166,7 @@ describe("CloudflareAIChatPlugin", () => { }); it("retains a settled turn until its queued response is observed", async () => { - plugin.enable(); + registerCloudflareAIChatInstrumentation(); const handlers = turnChannel().handlers(); const agent = { messages: [ @@ -225,7 +222,7 @@ describe("CloudflareAIChatPlugin", () => { it("drops retained turns that never produce a response", () => { vi.useFakeTimers(); try { - plugin.enable(); + registerCloudflareAIChatInstrumentation(); const handlers = turnChannel().handlers(); const agent = { messages: [], @@ -263,7 +260,7 @@ describe("CloudflareAIChatPlugin", () => { }); it("preserves pre-turn input when a continuation reuses its output id", async () => { - plugin.enable(); + registerCloudflareAIChatInstrumentation(); const handlers = turnChannel().handlers(); const agent = { messages: [ @@ -324,7 +321,7 @@ describe("CloudflareAIChatPlugin", () => { }); it("deduplicates nested manual and automatic turn events", () => { - plugin.enable(); + registerCloudflareAIChatInstrumentation(); const handlers = turnChannel().handlers(); const agent = { messages: [], onChatResponse() {} }; const outer = { @@ -348,8 +345,8 @@ describe("CloudflareAIChatPlugin", () => { expect(span.end).toHaveBeenCalledTimes(1); }); - it("logs original errors and closes outstanding spans on disable", () => { - plugin.enable(); + it("logs original errors without closing unrelated outstanding spans", () => { + registerCloudflareAIChatInstrumentation(); const handlers = turnChannel().handlers(); const failure = new Error("turn failed"); const failedEvent = { @@ -370,8 +367,7 @@ describe("CloudflareAIChatPlugin", () => { } as any; handlers.start?.(pendingEvent, "start"); const pendingSpan = mockStartSpan.mock.results[1].value; - plugin.disable(); - expect(pendingSpan.end).toHaveBeenCalledTimes(1); + expect(pendingSpan.end).not.toHaveBeenCalled(); }); function turnChannel() { @@ -394,7 +390,6 @@ function createMockChannel() { hasSubscribers: false, start: { bindStore: vi.fn(), - unbindStore: vi.fn(), }, subscribe: vi.fn((handlers) => subscribed.push(handlers)), traceSync: vi.fn((callback, event) => { @@ -410,6 +405,5 @@ function createMockChannel() { throw error; } }), - unsubscribe: vi.fn(), }; } diff --git a/js/src/instrumentation/plugins/cloudflare-ai-chat-plugin.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts similarity index 94% rename from js/src/instrumentation/plugins/cloudflare-ai-chat-plugin.ts rename to js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts index d875824af..a75ad3ea6 100644 --- a/js/src/instrumentation/plugins/cloudflare-ai-chat-plugin.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts @@ -1,4 +1,3 @@ -import { BasePlugin } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { @@ -41,7 +40,7 @@ type TurnState = { const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]); const RESPONSE_STATE_RETENTION_MS = 60_000; -export class CloudflareAIChatPlugin extends BasePlugin { +class CloudflareAIChatInstrumentationConsumer { private readonly activeStates = new Set(); private readonly activeTurns = new WeakMap< object, @@ -49,30 +48,18 @@ export class CloudflareAIChatPlugin extends BasePlugin { >(); private readonly eventStates = new WeakMap(); - protected onEnable(): void { + public register(): void { this.subscribeToResponseHook(); this.subscribeToTurnRunner(); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - - for (const state of this.activeStates) { - this.cleanupState(state); - } - this.activeStates.clear(); - } - private subscribeToTurnRunner(): void { const tracingChannel = cloudflareAIChatChannels.runExclusiveChatTurn.tracingChannel() as IsoTracingChannel< ChannelMessage >; - const unbindCurrentSpanStore = this.bindCurrentSpanStore(tracingChannel); + this.bindCurrentSpanStore(tracingChannel); const handlers: IsoChannelHandlers> = { start: (event) => { this.ensureEventState(event); @@ -86,10 +73,6 @@ export class CloudflareAIChatPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }); } private subscribeToResponseHook(): void { @@ -143,12 +126,11 @@ export class CloudflareAIChatPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => tracingChannel.unsubscribe(handlers)); } private bindCurrentSpanStore( tracingChannel: IsoTracingChannel>, - ): (() => void) | undefined { + ): void { const globalState = _internalGetGlobalState(); const contextManager = globalState?.contextManager; const startChannel = tracingChannel.start; @@ -161,7 +143,7 @@ export class CloudflareAIChatPlugin extends BasePlugin { : undefined; if (!startChannel || !currentSpanStore || !contextManager) { - return undefined; + return; } startChannel.bindStore(currentSpanStore, (event) => { @@ -170,8 +152,6 @@ export class CloudflareAIChatPlugin extends BasePlugin { ? contextManager.wrapSpanForStore(state.span) : currentSpanStore.getStore(); }); - - return () => startChannel.unbindStore(currentSpanStore); } private ensureEventState( @@ -481,3 +461,13 @@ function sanitizeLoggedValue( } return Object.fromEntries(entries); } + +let cloudflareAIChatInstrumentationConsumer: + | CloudflareAIChatInstrumentationConsumer + | undefined; + +export function registerCloudflareAIChatInstrumentation(): void { + cloudflareAIChatInstrumentationConsumer ??= + new CloudflareAIChatInstrumentationConsumer(); + cloudflareAIChatInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/cloudflare-ai-chat-instrumentation.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-instrumentation.ts similarity index 100% rename from js/src/instrumentation/plugins/cloudflare-ai-chat-instrumentation.ts rename to js/src/instrumentation/providers/cloudflare-ai-chat-instrumentation.ts diff --git a/js/src/instrumentation/plugins/cloudflare-think-channels.ts b/js/src/instrumentation/providers/cloudflare-think-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/cloudflare-think-channels.ts rename to js/src/instrumentation/providers/cloudflare-think-channels.ts diff --git a/js/src/instrumentation/plugins/cloudflare-think-context.ts b/js/src/instrumentation/providers/cloudflare-think-context.ts similarity index 100% rename from js/src/instrumentation/plugins/cloudflare-think-context.ts rename to js/src/instrumentation/providers/cloudflare-think-context.ts diff --git a/js/src/instrumentation/plugins/cloudflare-think-plugin.test.ts b/js/src/instrumentation/providers/cloudflare-think-instrumentation.test.ts similarity index 98% rename from js/src/instrumentation/plugins/cloudflare-think-plugin.test.ts rename to js/src/instrumentation/providers/cloudflare-think-instrumentation.test.ts index 240c33df0..f3e27919e 100644 --- a/js/src/instrumentation/plugins/cloudflare-think-plugin.test.ts +++ b/js/src/instrumentation/providers/cloudflare-think-instrumentation.test.ts @@ -27,7 +27,7 @@ describe("Cloudflare Think instrumentation", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "cloudflare-think-plugin.test.ts", + projectName: "cloudflare-think-instrumentation.test.ts", projectId: "test-project-id", }); }); diff --git a/js/src/instrumentation/plugins/cloudflare-think-plugin.ts b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts similarity index 93% rename from js/src/instrumentation/plugins/cloudflare-think-plugin.ts rename to js/src/instrumentation/providers/cloudflare-think-instrumentation.ts index 8a29eb16c..66196becf 100644 --- a/js/src/instrumentation/plugins/cloudflare-think-plugin.ts +++ b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts @@ -1,4 +1,3 @@ -import { BasePlugin } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { @@ -25,7 +24,7 @@ import { finalizeAISDKChildTracing, patchAISDKStreamingResult, prepareAISDKAgentCallInput, -} from "./ai-sdk-plugin"; +} from "./ai-sdk-instrumentation"; import { cloudflareThinkChannels } from "./cloudflare-think-channels"; import { registerCloudflareThinkSpan, @@ -50,27 +49,15 @@ type AISDKStreamEvent = const THINK_STATE_ID = Symbol.for("braintrust.cloudflare-think.state-id"); -export class CloudflareThinkPlugin extends BasePlugin { +class CloudflareThinkInstrumentationConsumer { private readonly statesBySpanId = new Map(); - protected onEnable(): void { + public register(): void { this.subscribeToThinkRuns(); this.subscribeToAISDKStreamTextSync(); this.subscribeToAISDKStreamTextAsync(); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - - for (const state of this.statesBySpanId.values()) { - this.finishState(state); - } - this.statesBySpanId.clear(); - } - private subscribeToThinkRuns(): void { const channel = cloudflareThinkChannels.runInferenceLoop.tracingChannel(); const states = new WeakMap(); @@ -137,9 +124,6 @@ export class CloudflareThinkPlugin extends BasePlugin { ? contextManager.wrapSpanForStore(runState.span) : currentSpanStore.getStore(); }); - this.unsubscribers.push(() => - channel.start?.unbindStore(currentSpanStore), - ); } const handlers: IsoChannelHandlers< @@ -166,7 +150,6 @@ export class CloudflareThinkPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => channel.unsubscribe(handlers)); } private subscribeToAISDKStreamTextSync(): void { @@ -186,7 +169,6 @@ export class CloudflareThinkPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => channel.unsubscribe(handlers)); } private subscribeToAISDKStreamTextAsync(): void { @@ -206,7 +188,6 @@ export class CloudflareThinkPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => channel.unsubscribe(handlers)); } private startAISDKStream(event: AISDKStreamEvent): void { @@ -382,3 +363,13 @@ function extractThinkTaskInput(input: unknown): unknown { } return input; } + +let cloudflareThinkInstrumentationConsumer: + | CloudflareThinkInstrumentationConsumer + | undefined; + +export function registerCloudflareThinkInstrumentation(): void { + cloudflareThinkInstrumentationConsumer ??= + new CloudflareThinkInstrumentationConsumer(); + cloudflareThinkInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/cohere-channels.ts b/js/src/instrumentation/providers/cohere-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/cohere-channels.ts rename to js/src/instrumentation/providers/cohere-channels.ts diff --git a/js/src/instrumentation/plugins/cohere-plugin.test.ts b/js/src/instrumentation/providers/cohere-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/cohere-plugin.test.ts rename to js/src/instrumentation/providers/cohere-instrumentation.test.ts index c2ea4fbf1..d7bd3603c 100644 --- a/js/src/instrumentation/plugins/cohere-plugin.test.ts +++ b/js/src/instrumentation/providers/cohere-instrumentation.test.ts @@ -3,7 +3,7 @@ import { aggregateCohereChatStreamChunks, extractCohereResponseMetadata, parseCohereMetricsFromUsage, -} from "./cohere-plugin"; +} from "./cohere-instrumentation"; describe("parseCohereMetricsFromUsage", () => { it("parses usage and meta token counters", () => { diff --git a/js/src/instrumentation/plugins/cohere-plugin.ts b/js/src/instrumentation/providers/cohere-instrumentation.ts similarity index 89% rename from js/src/instrumentation/plugins/cohere-plugin.ts rename to js/src/instrumentation/providers/cohere-instrumentation.ts index afaea628c..14e21179c 100644 --- a/js/src/instrumentation/plugins/cohere-plugin.ts +++ b/js/src/instrumentation/providers/cohere-instrumentation.ts @@ -1,8 +1,6 @@ -import { BasePlugin } from "../core"; import { traceAsyncChannel, traceStreamingChannel, - unsubscribeAll, } from "../core/channel-tracing"; import { SpanTypeAttribute, isObject } from "../../../util/index"; import { processInputAttachments } from "../../wrappers/attachment-utils"; @@ -16,79 +14,67 @@ import type { CohereUsageLike, } from "../../vendor-sdk-types/cohere"; -export class CoherePlugin extends BasePlugin { - protected onEnable(): void { +class CohereInstrumentationConsumer { + public register(): void { this.subscribeToCohereChannels(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } - private subscribeToCohereChannels(): void { - this.unsubscribers.push( - traceStreamingChannel(cohereChannels.chat, { - name: "cohere.chat", - type: SpanTypeAttribute.LLM, - extractInput: extractChatInputWithMetadata, - extractOutput: (result) => extractCohereChatOutput(result), - extractMetadata: (result) => extractCohereResponseMetadata(result), - extractMetrics: (result, startTime) => { - const metrics = parseCohereMetricsFromUsage(result); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - }), - ); + traceStreamingChannel(cohereChannels.chat, { + name: "cohere.chat", + type: SpanTypeAttribute.LLM, + extractInput: extractChatInputWithMetadata, + extractOutput: (result) => extractCohereChatOutput(result), + extractMetadata: (result) => extractCohereResponseMetadata(result), + extractMetrics: (result, startTime) => { + const metrics = parseCohereMetricsFromUsage(result); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + }); - this.unsubscribers.push( - traceStreamingChannel(cohereChannels.chatStream, { - name: "cohere.chatStream", - type: SpanTypeAttribute.LLM, - extractInput: extractChatInputWithMetadata, - extractOutput: () => undefined, - extractMetadata: () => undefined, - extractMetrics: () => ({}), - aggregateChunks: aggregateCohereChatStreamChunks, - }), - ); + traceStreamingChannel(cohereChannels.chatStream, { + name: "cohere.chatStream", + type: SpanTypeAttribute.LLM, + extractInput: extractChatInputWithMetadata, + extractOutput: () => undefined, + extractMetadata: () => undefined, + extractMetrics: () => ({}), + aggregateChunks: aggregateCohereChatStreamChunks, + }); - this.unsubscribers.push( - traceAsyncChannel(cohereChannels.embed, { - name: "cohere.embed", - type: SpanTypeAttribute.LLM, - extractInput: extractEmbedInputWithMetadata, - extractOutput: extractCohereEmbeddingOutput, - extractMetadata: (result) => extractCohereResponseMetadata(result), - extractMetrics: (result) => parseCohereMetricsFromUsage(result), - }), - ); + traceAsyncChannel(cohereChannels.embed, { + name: "cohere.embed", + type: SpanTypeAttribute.LLM, + extractInput: extractEmbedInputWithMetadata, + extractOutput: extractCohereEmbeddingOutput, + extractMetadata: (result) => extractCohereResponseMetadata(result), + extractMetrics: (result) => parseCohereMetricsFromUsage(result), + }); - this.unsubscribers.push( - traceAsyncChannel(cohereChannels.rerank, { - name: "cohere.rerank", - type: SpanTypeAttribute.LLM, - extractInput: extractRerankInputWithMetadata, - extractOutput: (result) => { - if (!isObject(result) || !Array.isArray(result.results)) { - return undefined; - } - - return result.results.slice(0, 100).map((item) => ({ - index: isObject(item) ? item.index : undefined, - relevance_score: isObject(item) - ? ((typeof item.relevanceScore === "number" - ? item.relevanceScore - : item.relevance_score) ?? null) - : null, - })); - }, - extractMetadata: (result) => extractCohereResponseMetadata(result), - extractMetrics: (result) => parseCohereMetricsFromUsage(result), - }), - ); + traceAsyncChannel(cohereChannels.rerank, { + name: "cohere.rerank", + type: SpanTypeAttribute.LLM, + extractInput: extractRerankInputWithMetadata, + extractOutput: (result) => { + if (!isObject(result) || !Array.isArray(result.results)) { + return undefined; + } + + return result.results.slice(0, 100).map((item) => ({ + index: isObject(item) ? item.index : undefined, + relevance_score: isObject(item) + ? ((typeof item.relevanceScore === "number" + ? item.relevanceScore + : item.relevance_score) ?? null) + : null, + })); + }, + extractMetadata: (result) => extractCohereResponseMetadata(result), + extractMetrics: (result) => parseCohereMetricsFromUsage(result), + }); } } @@ -949,3 +935,10 @@ export function aggregateCohereChatStreamChunks( output, }; } + +let cohereInstrumentationConsumer: CohereInstrumentationConsumer | undefined; + +export function registerCohereInstrumentation(): void { + cohereInstrumentationConsumer ??= new CohereInstrumentationConsumer(); + cohereInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/cursor-sdk-channels.ts b/js/src/instrumentation/providers/cursor-sdk-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/cursor-sdk-channels.ts rename to js/src/instrumentation/providers/cursor-sdk-channels.ts diff --git a/js/src/instrumentation/plugins/cursor-sdk-plugin.test.ts b/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts similarity index 94% rename from js/src/instrumentation/plugins/cursor-sdk-plugin.test.ts rename to js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts index b22052c8c..c30169c35 100644 --- a/js/src/instrumentation/plugins/cursor-sdk-plugin.test.ts +++ b/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts @@ -15,11 +15,11 @@ vi.mock("../../logger", () => ({ })); import iso from "../../isomorph"; -import { CursorSDKPlugin } from "./cursor-sdk-plugin"; +import { registerCursorSDKInstrumentation } from "./cursor-sdk-instrumentation"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; -describe("CursorSDKPlugin", () => { +describe("registerCursorSDKInstrumentation", () => { let handlersByName: Map; let spans: Array<{ end: ReturnType; @@ -34,7 +34,6 @@ describe("CursorSDKPlugin", () => { mockNewTracingChannel.mockImplementation((name: string) => ({ subscribe: vi.fn((handlers) => handlersByName.set(name, handlers)), tracePromise: vi.fn((fn) => fn()), - unsubscribe: vi.fn(), })); mockStartSpan.mockImplementation((args: any) => { const span = { @@ -56,9 +55,7 @@ describe("CursorSDKPlugin", () => { }); it("subscribes to Cursor SDK channels", () => { - const plugin = new CursorSDKPlugin(); - - plugin.enable(); + registerCursorSDKInstrumentation(); expect(handlersByName.has("orchestrion:@cursor/sdk:Agent.create")).toBe( true, @@ -73,8 +70,7 @@ describe("CursorSDKPlugin", () => { }); it("patches agents returned by Agent.create and traces send/wait", async () => { - const plugin = new CursorSDKPlugin(); - plugin.enable(); + registerCursorSDKInstrumentation(); const createHandlers = handlersByName.get( "orchestrion:@cursor/sdk:Agent.create", @@ -131,8 +127,7 @@ describe("CursorSDKPlugin", () => { }); it("captures stream tool calls and usage", async () => { - const plugin = new CursorSDKPlugin(); - plugin.enable(); + registerCursorSDKInstrumentation(); const sendHandlers = handlersByName.get( "orchestrion:@cursor/sdk:agent.send", @@ -214,8 +209,7 @@ describe("CursorSDKPlugin", () => { }); it("traces Agent.prompt without a nested send span", () => { - const plugin = new CursorSDKPlugin(); - plugin.enable(); + registerCursorSDKInstrumentation(); const promptHandlers = handlersByName.get( "orchestrion:@cursor/sdk:Agent.prompt", diff --git a/js/src/instrumentation/plugins/cursor-sdk-plugin.ts b/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts similarity index 98% rename from js/src/instrumentation/plugins/cursor-sdk-plugin.ts rename to js/src/instrumentation/providers/cursor-sdk-instrumentation.ts index f01db5987..41118be17 100644 --- a/js/src/instrumentation/plugins/cursor-sdk-plugin.ts +++ b/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts @@ -1,4 +1,4 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; @@ -64,23 +64,15 @@ type PromptState = { startTime: number; }; -export class CursorSDKPlugin extends BasePlugin { +class CursorSDKInstrumentationConsumer { private promptDepth = 0; - protected onEnable(): void { + public register(): void { this.subscribeToAgentFactories(); this.subscribeToPrompt(); this.subscribeToSend(); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - this.promptDepth = 0; - } - private subscribeToAgentFactories(): void { this.subscribeToAgentFactory(cursorSDKChannels.create); this.subscribeToAgentFactory(cursorSDKChannels.resume); @@ -98,9 +90,6 @@ export class CursorSDKPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - tracingChannel.unsubscribe(handlers); - }); } private subscribeToPrompt(): void { @@ -171,9 +160,6 @@ export class CursorSDKPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => { - channel.unsubscribe(handlers); - }); } private subscribeToSend(): void { @@ -267,9 +253,6 @@ export class CursorSDKPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => { - channel.unsubscribe(handlers); - }); } } @@ -1185,3 +1168,12 @@ function cleanMetrics(metrics: Record): Record { } return cleaned; } + +let cursorSDKInstrumentationConsumer: + | CursorSDKInstrumentationConsumer + | undefined; + +export function registerCursorSDKInstrumentation(): void { + cursorSDKInstrumentationConsumer ??= new CursorSDKInstrumentationConsumer(); + cursorSDKInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/eve-plugin.test.ts b/js/src/instrumentation/providers/eve-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/eve-plugin.test.ts rename to js/src/instrumentation/providers/eve-instrumentation.test.ts index 38e15bef0..9e1a6bc28 100644 --- a/js/src/instrumentation/plugins/eve-plugin.test.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.test.ts @@ -11,7 +11,7 @@ import * as braintrustExports from "../../exports"; import { configureNode } from "../../node/config"; import { Attachment, _exportsForTestingOnly, initLogger } from "../../logger"; import * as instrumentationExports from "../index"; -import { braintrustEveInstrumentation } from "./eve-plugin"; +import { braintrustEveInstrumentation } from "./eve-instrumentation"; import type { EveInstrumentationAttemptScope, EveInstrumentationHandlerContext, @@ -60,7 +60,7 @@ describe("braintrustEveInstrumentation", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "eve-plugin.test.ts", + projectName: "eve-instrumentation.test.ts", projectId: "test-project-id", }); }); @@ -107,7 +107,7 @@ describe("braintrustEveInstrumentation", () => { it("records a turn with LLM and action spans", async () => { const provider = braintrustEveInstrumentation({ metadata: { - scenario: "eve-plugin-unit", + scenario: "eve-instrumentation-unit", testRunId: "test-run-flat-tree", }, }); @@ -272,7 +272,7 @@ describe("braintrustEveInstrumentation", () => { input: [{ content: "Search then read", role: "user" }], metadata: { "eve.session_id": "session-root", - scenario: "eve-plugin-unit", + scenario: "eve-instrumentation-unit", testRunId: "test-run-flat-tree", }, output: "Here is the Eve instrumentation guide.", @@ -300,7 +300,7 @@ describe("braintrustEveInstrumentation", () => { "eve.session_id": "session-root", model: "qwen/qwen3-30b-a3b", provider: "openrouter", - scenario: "eve-plugin-unit", + scenario: "eve-instrumentation-unit", testRunId: "test-run-flat-tree", }, span_attributes: { @@ -345,7 +345,7 @@ describe("braintrustEveInstrumentation", () => { input: { query: "Eve instrumentation" }, metadata: { "eve.session_id": "session-root", - scenario: "eve-plugin-unit", + scenario: "eve-instrumentation-unit", testRunId: "test-run-flat-tree", }, output: { hits: ["eve.dev/docs"] }, diff --git a/js/src/instrumentation/plugins/eve-plugin.ts b/js/src/instrumentation/providers/eve-instrumentation.ts similarity index 100% rename from js/src/instrumentation/plugins/eve-plugin.ts rename to js/src/instrumentation/providers/eve-instrumentation.ts diff --git a/js/src/instrumentation/plugins/flue-plugin.test.ts b/js/src/instrumentation/providers/flue-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/flue-plugin.test.ts rename to js/src/instrumentation/providers/flue-instrumentation.test.ts index 04901f8cc..ae6fbec8b 100644 --- a/js/src/instrumentation/plugins/flue-plugin.test.ts +++ b/js/src/instrumentation/providers/flue-instrumentation.test.ts @@ -64,7 +64,7 @@ vi.mock("../../logger", () => ({ }, })); -import { braintrustFlueInstrumentation } from "./flue-plugin"; +import { braintrustFlueInstrumentation } from "./flue-instrumentation"; describe("Flue instrumentation", () => { let spans: Array<{ diff --git a/js/src/instrumentation/plugins/flue-plugin.ts b/js/src/instrumentation/providers/flue-instrumentation.ts similarity index 100% rename from js/src/instrumentation/plugins/flue-plugin.ts rename to js/src/instrumentation/providers/flue-instrumentation.ts diff --git a/js/src/instrumentation/plugins/genkit-channels.ts b/js/src/instrumentation/providers/genkit-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/genkit-channels.ts rename to js/src/instrumentation/providers/genkit-channels.ts diff --git a/js/src/instrumentation/plugins/genkit-plugin.test.ts b/js/src/instrumentation/providers/genkit-instrumentation.test.ts similarity index 91% rename from js/src/instrumentation/plugins/genkit-plugin.test.ts rename to js/src/instrumentation/providers/genkit-instrumentation.test.ts index dcf41c0f7..11f172e5b 100644 --- a/js/src/instrumentation/plugins/genkit-plugin.test.ts +++ b/js/src/instrumentation/providers/genkit-instrumentation.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { _exportsForTestingOnly, initLogger } from "../../logger"; -import { GenkitPlugin } from "./genkit-plugin"; +import { registerGenkitInstrumentation } from "./genkit-instrumentation"; import { genkitChannels } from "./genkit-channels"; function singleQueueStream( @@ -35,9 +35,7 @@ async function collectAsync(stream: AsyncIterable): Promise { return chunks; } -describe("GenkitPlugin stream patching", () => { - const plugin = new GenkitPlugin(); - +describe("registerGenkitInstrumentation stream patching", () => { beforeAll(async () => { await _exportsForTestingOnly.simulateLoginForTests(); }); @@ -45,18 +43,17 @@ describe("GenkitPlugin stream patching", () => { beforeEach(() => { _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "genkit-plugin.test.ts", + projectName: "genkit-instrumentation.test.ts", projectId: "test-project-id", }); }); afterEach(() => { - plugin.disable(); _exportsForTestingOnly.clearTestBackgroundLogger(); }); it("does not consume generateStream chunks before user code reads them", async () => { - plugin.enable(); + registerGenkitInstrumentation(); const stream = singleQueueStream([{ text: "hello" }, { text: " world" }]); const result = genkitChannels.generateStream.traceSync( @@ -85,7 +82,7 @@ describe("GenkitPlugin stream patching", () => { }); it("does not consume action.stream chunks before user code reads them", async () => { - plugin.enable(); + registerGenkitInstrumentation(); const stream = singleQueueStream(["first", "second"]); const action = Object.assign(() => Promise.resolve(), { __action: { diff --git a/js/src/instrumentation/plugins/genkit-plugin.ts b/js/src/instrumentation/providers/genkit-instrumentation.ts similarity index 85% rename from js/src/instrumentation/plugins/genkit-plugin.ts rename to js/src/instrumentation/providers/genkit-instrumentation.ts index 90edb66e4..e485295ea 100644 --- a/js/src/instrumentation/plugins/genkit-plugin.ts +++ b/js/src/instrumentation/providers/genkit-instrumentation.ts @@ -1,8 +1,7 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import { traceAsyncChannel, traceSyncStreamChannel, - unsubscribeAll, } from "../core/channel-tracing"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { ChannelMessage } from "../core/channel-definitions"; @@ -38,61 +37,49 @@ type SpanState = { startTime: number; }; -export class GenkitPlugin extends BasePlugin { - protected onEnable(): void { +class GenkitInstrumentationConsumer { + public register(): void { this.subscribeToGenkitChannels(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } - private subscribeToGenkitChannels(): void { - this.unsubscribers.push( - traceAsyncChannel(genkitChannels.generate, { - name: "genkit.generate", - type: SpanTypeAttribute.LLM, - extractInput: ([input]) => extractGenerateInput(input), - extractOutput: extractGenerateOutput, - extractMetadata: (result, event) => - extractGenerateResponseMetadata(result, event?.arguments?.[0]), - extractMetrics: (result) => parseGenkitUsageMetrics(result?.usage), - }), - ); + traceAsyncChannel(genkitChannels.generate, { + name: "genkit.generate", + type: SpanTypeAttribute.LLM, + extractInput: ([input]) => extractGenerateInput(input), + extractOutput: extractGenerateOutput, + extractMetadata: (result, event) => + extractGenerateResponseMetadata(result, event?.arguments?.[0]), + extractMetrics: (result) => parseGenkitUsageMetrics(result?.usage), + }); - this.unsubscribers.push( - traceSyncStreamChannel(genkitChannels.generateStream, { - name: "genkit.generateStream", - type: SpanTypeAttribute.LLM, - extractInput: ([input]) => extractGenerateInput(input), - patchResult: ({ result, span, startTime }) => - patchGenerateStreamResult(result, span, startTime), - }), - ); + traceSyncStreamChannel(genkitChannels.generateStream, { + name: "genkit.generateStream", + type: SpanTypeAttribute.LLM, + extractInput: ([input]) => extractGenerateInput(input), + patchResult: ({ result, span, startTime }) => + patchGenerateStreamResult(result, span, startTime), + }); - this.unsubscribers.push( - traceAsyncChannel(genkitChannels.embed, { - name: "genkit.embed", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params]) => extractEmbedInput(params), - extractOutput: (result) => summarizeEmbeddingResult(result), - extractMetadata: (_result, event) => - extractEmbedMetadata(event?.arguments?.[0]), - extractMetrics: () => ({}), - }), - ); + traceAsyncChannel(genkitChannels.embed, { + name: "genkit.embed", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params]) => extractEmbedInput(params), + extractOutput: (result) => summarizeEmbeddingResult(result), + extractMetadata: (_result, event) => + extractEmbedMetadata(event?.arguments?.[0]), + extractMetrics: () => ({}), + }); - this.unsubscribers.push( - traceAsyncChannel(genkitChannels.embedMany, { - name: "genkit.embedMany", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params]) => extractEmbedManyInput(params), - extractOutput: summarizeEmbeddingResult, - extractMetadata: (_result, event) => - extractEmbedMetadata(event?.arguments?.[0]), - extractMetrics: () => ({}), - }), - ); + traceAsyncChannel(genkitChannels.embedMany, { + name: "genkit.embedMany", + type: SpanTypeAttribute.FUNCTION, + extractInput: ([params]) => extractEmbedManyInput(params), + extractOutput: summarizeEmbeddingResult, + extractMetadata: (_result, event) => + extractEmbedMetadata(event?.arguments?.[0]), + extractMetrics: () => ({}), + }); this.subscribeToActionRun(); this.subscribeToActionSpan(); @@ -105,10 +92,8 @@ export class GenkitPlugin extends BasePlugin { ChannelMessage >; const states = new WeakMap(); - const unbindCurrentSpanStore = bindActionCurrentSpanStoreToStart( - tracingChannel, - states, - (event) => startActionRunSpan(event), + bindActionCurrentSpanStoreToStart(tracingChannel, states, (event) => + startActionRunSpan(event), ); const handlers: IsoChannelHandlers< @@ -147,10 +132,6 @@ export class GenkitPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }); } private subscribeToActionSpan(): void { @@ -159,10 +140,8 @@ export class GenkitPlugin extends BasePlugin { ChannelMessage >; const states = new WeakMap(); - const unbindCurrentSpanStore = bindActionCurrentSpanStoreToStart( - tracingChannel, - states, - (event) => startActionSpan(event), + bindActionCurrentSpanStoreToStart(tracingChannel, states, (event) => + startActionSpan(event), ); const handlers: IsoChannelHandlers< @@ -202,25 +181,19 @@ export class GenkitPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }); } private subscribeToActionStream(): void { - this.unsubscribers.push( - traceSyncStreamChannel(genkitChannels.actionStream, { - name: "genkit.action.stream", - type: SpanTypeAttribute.TASK, - extractInput: ([input], event) => ({ - input, - metadata: actionMetadataForLog(extractActionMetadata(event.self)), - }), - patchResult: ({ result, span, startTime }) => - patchActionStreamResult(result, span, startTime), + traceSyncStreamChannel(genkitChannels.actionStream, { + name: "genkit.action.stream", + type: SpanTypeAttribute.TASK, + extractInput: ([input], event) => ({ + input, + metadata: actionMetadataForLog(extractActionMetadata(event.self)), }), - ); + patchResult: ({ result, span, startTime }) => + patchActionStreamResult(result, span, startTime), + }); } } @@ -307,7 +280,7 @@ function bindActionCurrentSpanStoreToStart< tracingChannel: IsoTracingChannel>, states: WeakMap, create: (event: ChannelMessage) => SpanState | undefined, -): (() => void) | undefined { +): void { const state = _internalGetGlobalState(); const contextManager = state?.contextManager; const startChannel = tracingChannel.start as @@ -316,7 +289,6 @@ function bindActionCurrentSpanStoreToStart< store: CurrentSpanStore, callback: (event: ChannelMessage) => unknown, ) => void; - unbindStore?: (store: CurrentSpanStore) => void; } & object) | undefined; const currentSpanStore = contextManager @@ -328,7 +300,7 @@ function bindActionCurrentSpanStoreToStart< : undefined; if (!startChannel?.bindStore || !currentSpanStore) { - return undefined; + return; } startChannel.bindStore(currentSpanStore, (event) => { @@ -339,10 +311,6 @@ function bindActionCurrentSpanStoreToStart< ? contextManager!.wrapSpanForStore(state.span) : currentSpanStore.getStore(); }); - - return () => { - startChannel.unbindStore?.(currentSpanStore); - }; } function normalizeInput(input: GenkitGenerateInput): GenkitGenerateInput { @@ -771,3 +739,10 @@ function pickNumberMetrics( function stringValue(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } + +let genkitInstrumentationConsumer: GenkitInstrumentationConsumer | undefined; + +export function registerGenkitInstrumentation(): void { + genkitInstrumentationConsumer ??= new GenkitInstrumentationConsumer(); + genkitInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/github-copilot-channels.ts b/js/src/instrumentation/providers/github-copilot-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/github-copilot-channels.ts rename to js/src/instrumentation/providers/github-copilot-channels.ts diff --git a/js/src/instrumentation/plugins/github-copilot-plugin.test.ts b/js/src/instrumentation/providers/github-copilot-instrumentation.test.ts similarity index 79% rename from js/src/instrumentation/plugins/github-copilot-plugin.test.ts rename to js/src/instrumentation/providers/github-copilot-instrumentation.test.ts index 17ecb38d8..5e88b3b51 100644 --- a/js/src/instrumentation/plugins/github-copilot-plugin.test.ts +++ b/js/src/instrumentation/providers/github-copilot-instrumentation.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; import { - GitHubCopilotPlugin, + registerGitHubCopilotInstrumentation, extractMetricsFromUsage, -} from "./github-copilot-plugin"; +} from "./github-copilot-instrumentation"; describe("extractMetricsFromUsage", () => { it("maps input/output tokens to standard metric keys", () => { @@ -119,38 +119,8 @@ describe("extractMetricsFromUsage", () => { }); }); -describe("GitHubCopilotPlugin lifecycle", () => { - it("enables without throwing", () => { - const plugin = new GitHubCopilotPlugin(); - expect(() => plugin.enable()).not.toThrow(); - plugin.disable(); - }); - - it("disables without throwing", () => { - const plugin = new GitHubCopilotPlugin(); - plugin.enable(); - expect(() => plugin.disable()).not.toThrow(); - }); - - it("enable is idempotent", () => { - const plugin = new GitHubCopilotPlugin(); - plugin.enable(); - expect(() => plugin.enable()).not.toThrow(); - plugin.disable(); - }); - - it("disable is idempotent", () => { - const plugin = new GitHubCopilotPlugin(); - plugin.enable(); - plugin.disable(); - expect(() => plugin.disable()).not.toThrow(); - }); - - it("can be re-enabled after disable", () => { - const plugin = new GitHubCopilotPlugin(); - plugin.enable(); - plugin.disable(); - expect(() => plugin.enable()).not.toThrow(); - plugin.disable(); +describe("registerGitHubCopilotInstrumentation", () => { + it("registers without throwing", () => { + expect(() => registerGitHubCopilotInstrumentation()).not.toThrow(); }); }); diff --git a/js/src/instrumentation/plugins/github-copilot-plugin.ts b/js/src/instrumentation/providers/github-copilot-instrumentation.ts similarity index 97% rename from js/src/instrumentation/plugins/github-copilot-plugin.ts rename to js/src/instrumentation/providers/github-copilot-instrumentation.ts index 8fa156cfc..b5f14976f 100644 --- a/js/src/instrumentation/plugins/github-copilot-plugin.ts +++ b/js/src/instrumentation/providers/github-copilot-instrumentation.ts @@ -1,4 +1,3 @@ -import { BasePlugin } from "../core"; import type { IsoChannelHandlers } from "../../isomorph"; import { startSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; @@ -672,7 +671,7 @@ function isGitHubCopilotSession(value: unknown): value is GitHubCopilotSession { } // --------------------------------------------------------------------------- -// Plugin and handler factory +// Consumer and handler factory // --------------------------------------------------------------------------- // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -756,18 +755,11 @@ function makeSessionHandlers( }; } -export class GitHubCopilotPlugin extends BasePlugin { - protected onEnable(): void { +class GitHubCopilotInstrumentationConsumer { + public register(): void { this.subscribeToSessionChannels(); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - } - private subscribeToSessionChannels(): void { const createChannel = gitHubCopilotChannels.createSession.tracingChannel(); const resumeChannel = gitHubCopilotChannels.resumeSession.tracingChannel(); @@ -787,10 +779,15 @@ export class GitHubCopilotPlugin extends BasePlugin { createChannel.subscribe(createHandlers); resumeChannel.subscribe(resumeHandlers); - - this.unsubscribers.push( - () => createChannel.unsubscribe(createHandlers), - () => resumeChannel.unsubscribe(resumeHandlers), - ); } } + +let gitHubCopilotInstrumentationConsumer: + | GitHubCopilotInstrumentationConsumer + | undefined; + +export function registerGitHubCopilotInstrumentation(): void { + gitHubCopilotInstrumentationConsumer ??= + new GitHubCopilotInstrumentationConsumer(); + gitHubCopilotInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/google-adk-channels.ts b/js/src/instrumentation/providers/google-adk-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/google-adk-channels.ts rename to js/src/instrumentation/providers/google-adk-channels.ts diff --git a/js/src/instrumentation/plugins/google-adk-plugin.test.ts b/js/src/instrumentation/providers/google-adk-instrumentation.test.ts similarity index 91% rename from js/src/instrumentation/plugins/google-adk-plugin.test.ts rename to js/src/instrumentation/providers/google-adk-instrumentation.test.ts index 073af75f4..f4efcfc7d 100644 --- a/js/src/instrumentation/plugins/google-adk-plugin.test.ts +++ b/js/src/instrumentation/providers/google-adk-instrumentation.test.ts @@ -15,7 +15,7 @@ vi.mock("../../isomorph", () => ({ }, })); -import { GoogleADKPlugin } from "./google-adk-plugin"; +import { registerGoogleADKInstrumentation } from "./google-adk-instrumentation"; import iso from "../../isomorph"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; @@ -44,26 +44,19 @@ vi.mock("../../logger", () => ({ }, })); -describe("GoogleADKPlugin", () => { - let plugin: GoogleADKPlugin; +describe("registerGoogleADKInstrumentation", () => { let mockChannel: any; let subscribeSpy: any; - let unsubscribeSpy: any; let bindStoreSpy: any; - let unbindStoreSpy: any; beforeEach(() => { subscribeSpy = vi.fn(); - unsubscribeSpy = vi.fn(); bindStoreSpy = vi.fn(); - unbindStoreSpy = vi.fn(); mockChannel = { subscribe: subscribeSpy, - unsubscribe: unsubscribeSpy, hasSubscribers: false, start: { bindStore: bindStoreSpy, - unbindStore: unbindStoreSpy, }, }; @@ -71,16 +64,15 @@ describe("GoogleADKPlugin", () => { mockStartSpan.mockClear(); mockInternalGetGlobalState.mockReset(); mockInternalGetGlobalState.mockReturnValue(undefined); - plugin = new GoogleADKPlugin(); }); afterEach(() => { vi.clearAllMocks(); }); - describe("enable/disable lifecycle", () => { - it("should subscribe to channels when enabled", () => { - plugin.enable(); + describe("registration", () => { + it("subscribes to channels", () => { + registerGoogleADKInstrumentation(); // Should subscribe to 3 channels: runner.runAsync, agent.runAsync, tool.runAsync expect(mockNewTracingChannel).toHaveBeenCalledWith( @@ -94,43 +86,11 @@ describe("GoogleADKPlugin", () => { ); expect(subscribeSpy).toHaveBeenCalledTimes(3); }); - - it("should not subscribe multiple times if enabled twice", () => { - plugin.enable(); - const firstCallCount = subscribeSpy.mock.calls.length; - - plugin.enable(); - const secondCallCount = subscribeSpy.mock.calls.length; - - expect(firstCallCount).toBe(secondCallCount); - }); - - it("should unsubscribe from channels when disabled", () => { - plugin.enable(); - plugin.disable(); - - expect(unsubscribeSpy).toHaveBeenCalled(); - }); - - it("should clear unsubscribers array after disable", () => { - plugin.enable(); - plugin.disable(); - - // Enable again should re-subscribe - subscribeSpy.mockClear(); - plugin.enable(); - - expect(subscribeSpy).toHaveBeenCalledTimes(3); - }); - - it("should not crash when disabled without being enabled", () => { - expect(() => plugin.disable()).not.toThrow(); - }); }); describe("runner.runAsync channel", () => { it("should create a TASK span with runner metadata on start", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); // Find the first subscribe call (runner channel) const handlers = subscribeSpy.mock.calls[0][0]; @@ -163,7 +123,7 @@ describe("GoogleADKPlugin", () => { }); it("should handle stream end with async iterable result", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); const handlers = subscribeSpy.mock.calls[0][0]; const event: any = { @@ -196,7 +156,7 @@ describe("GoogleADKPlugin", () => { }); it("should handle error events", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); const handlers = subscribeSpy.mock.calls[0][0]; const event: any = { @@ -228,7 +188,7 @@ describe("GoogleADKPlugin", () => { }, } as any); - plugin.enable(); + registerGoogleADKInstrumentation(); expect(bindStoreSpy).toHaveBeenNthCalledWith( 1, @@ -367,7 +327,7 @@ describe("GoogleADKPlugin", () => { ])( "normalizes $name", async ({ usageMetadata, expectedMetrics, absentMetrics }) => { - plugin.enable(); + registerGoogleADKInstrumentation(); const handlers = subscribeSpy.mock.calls[0][0]; const event: any = { @@ -398,7 +358,7 @@ describe("GoogleADKPlugin", () => { ); it("preserves explicitly reported zero usage", async () => { - plugin.enable(); + registerGoogleADKInstrumentation(); const handlers = subscribeSpy.mock.calls[0][0]; const event: any = { @@ -447,7 +407,7 @@ describe("GoogleADKPlugin", () => { describe("agent.runAsync channel", () => { it("should create a TASK span with agent metadata on start", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); // Agent channel is the second subscribe call const handlers = subscribeSpy.mock.calls[1][0]; @@ -474,7 +434,7 @@ describe("GoogleADKPlugin", () => { }); it("uses the invoked agent instance for names when parent context still points at a parent agent", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); const handlers = subscribeSpy.mock.calls[1][0]; const event = { @@ -511,7 +471,7 @@ describe("GoogleADKPlugin", () => { }); it("should handle agent without a name gracefully", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); const handlers = subscribeSpy.mock.calls[1][0]; const event = { @@ -537,7 +497,7 @@ describe("GoogleADKPlugin", () => { }, } as any); - plugin.enable(); + registerGoogleADKInstrumentation(); expect(bindStoreSpy).toHaveBeenNthCalledWith( 2, @@ -588,7 +548,7 @@ describe("GoogleADKPlugin", () => { .mockImplementationOnce(() => runnerSpan) .mockImplementationOnce(() => agentSpan); - plugin.enable(); + registerGoogleADKInstrumentation(); const runnerHandlers = subscribeSpy.mock.calls[0][0]; const agentHandlers = subscribeSpy.mock.calls[1][0]; @@ -638,7 +598,7 @@ describe("GoogleADKPlugin", () => { describe("tool.runAsync channel", () => { it("should create a TOOL span on start", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); // Tool channel is the third subscribe call const handlers = subscribeSpy.mock.calls[2][0]; @@ -675,7 +635,7 @@ describe("GoogleADKPlugin", () => { }); it("should log output and metrics on asyncEnd", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); const handlers = subscribeSpy.mock.calls[2][0]; const event: any = { @@ -706,7 +666,7 @@ describe("GoogleADKPlugin", () => { }); it("should handle tool execution errors", () => { - plugin.enable(); + registerGoogleADKInstrumentation(); const handlers = subscribeSpy.mock.calls[2][0]; const event: any = { diff --git a/js/src/instrumentation/plugins/google-adk-plugin.ts b/js/src/instrumentation/providers/google-adk-instrumentation.ts similarity index 95% rename from js/src/instrumentation/plugins/google-adk-plugin.ts rename to js/src/instrumentation/providers/google-adk-instrumentation.ts index 623264016..994779305 100644 --- a/js/src/instrumentation/plugins/google-adk-plugin.ts +++ b/js/src/instrumentation/providers/google-adk-instrumentation.ts @@ -1,4 +1,3 @@ -import { BasePlugin } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; @@ -51,9 +50,9 @@ type GoogleADKStreamChannel = | typeof googleADKChannels.agentRunAsync; /** - * Auto-instrumentation plugin for the Google ADK. + * Internal auto-instrumentation consumer for the Google ADK. * - * This plugin subscribes to orchestrion channels for Google ADK methods + * This consumer subscribes to orchestrion channels for Google ADK methods * and creates Braintrust spans to track: * - Runner.runAsync — top-level agent execution (TASK span) * - BaseAgent.runAsync — individual agent invocations (TASK span) @@ -62,25 +61,16 @@ type GoogleADKStreamChannel = * LLM calls made through ADK are automatically captured by the existing * @google/genai instrumentation since ADK uses GenAI internally. */ -export class GoogleADKPlugin extends BasePlugin { +class GoogleADKInstrumentationConsumer { private activeRunnerSpans = new Map(); private activeAgentSpans = new Map(); - protected onEnable(): void { + public register(): void { this.subscribeToRunnerRunAsync(); this.subscribeToAgentRunAsync(); this.subscribeToToolRunAsync(); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - this.activeRunnerSpans.clear(); - this.activeAgentSpans.clear(); - } - private subscribeToRunnerRunAsync(): void { const tracingChannel = googleADKChannels.runnerRunAsync.tracingChannel() as IsoTracingChannel< @@ -126,11 +116,7 @@ export class GoogleADKPlugin extends BasePlugin { return { span, startTime, events: [], contextKey }; }; - const unbindCurrentSpanStore = bindCurrentSpanStoreToStart( - tracingChannel, - states, - createState, - ); + bindCurrentSpanStoreToStart(tracingChannel, states, createState); const handlers: IsoChannelHandlers< ChannelMessage @@ -189,10 +175,6 @@ export class GoogleADKPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }); } private subscribeToAgentRunAsync(): void { @@ -262,11 +244,7 @@ export class GoogleADKPlugin extends BasePlugin { return { span, startTime, events: [], contextKey, name: agentName }; }; - const unbindCurrentSpanStore = bindCurrentSpanStoreToStart( - tracingChannel, - states, - createState, - ); + bindCurrentSpanStoreToStart(tracingChannel, states, createState); const handlers: IsoChannelHandlers< ChannelMessage @@ -324,10 +302,6 @@ export class GoogleADKPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }); } private subscribeToToolRunAsync(): void { @@ -413,9 +387,6 @@ export class GoogleADKPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - tracingChannel.unsubscribe(handlers); - }); } } @@ -536,7 +507,7 @@ function bindCurrentSpanStoreToStart< tracingChannel: IsoTracingChannel>, states: WeakMap, create: (event: ChannelMessage) => TState, -): (() => void) | undefined { +): void { const state = _internalGetGlobalState(); const contextManager = state?.contextManager; const startChannel = tracingChannel.start as @@ -545,7 +516,6 @@ function bindCurrentSpanStoreToStart< store: CurrentSpanStore, callback: (event: ChannelMessage) => unknown, ) => void; - unbindStore?: (store: CurrentSpanStore) => void; } & object) | undefined; const currentSpanStore = contextManager @@ -557,7 +527,7 @@ function bindCurrentSpanStoreToStart< : undefined; if (!startChannel?.bindStore || !currentSpanStore) { - return undefined; + return; } startChannel.bindStore(currentSpanStore, (event) => { @@ -566,10 +536,6 @@ function bindCurrentSpanStoreToStart< ).span; return contextManager.wrapSpanForStore(span); }); - - return () => { - startChannel.unbindStore?.(currentSpanStore); - }; } // ---- Helper functions ---- @@ -1026,3 +992,12 @@ function cleanMetrics(metrics: Record): Record { } return cleaned; } + +let googleADKInstrumentationConsumer: + | GoogleADKInstrumentationConsumer + | undefined; + +export function registerGoogleADKInstrumentation(): void { + googleADKInstrumentationConsumer ??= new GoogleADKInstrumentationConsumer(); + googleADKInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/google-genai-channels.ts b/js/src/instrumentation/providers/google-genai-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/google-genai-channels.ts rename to js/src/instrumentation/providers/google-genai-channels.ts diff --git a/js/src/instrumentation/plugins/google-genai-plugin.test.ts b/js/src/instrumentation/providers/google-genai-instrumentation.test.ts similarity index 95% rename from js/src/instrumentation/plugins/google-genai-plugin.test.ts rename to js/src/instrumentation/providers/google-genai-instrumentation.test.ts index f14690e73..175cfb309 100644 --- a/js/src/instrumentation/plugins/google-genai-plugin.test.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.test.ts @@ -25,7 +25,7 @@ vi.mock("../../isomorph", () => ({ }, })); -import { GoogleGenAIPlugin } from "./google-genai-plugin"; +import { registerGoogleGenAIInstrumentation } from "./google-genai-instrumentation"; import { startSpan } from "../../logger"; import iso from "../../isomorph"; @@ -50,66 +50,27 @@ vi.mock("../../logger", () => ({ }, })); -describe("GoogleGenAIPlugin", () => { - let plugin: GoogleGenAIPlugin; +describe("registerGoogleGenAIInstrumentation", () => { let mockChannel: any; let subscribeSpy: any; - let unsubscribeSpy: any; beforeEach(() => { subscribeSpy = vi.fn(); - unsubscribeSpy = vi.fn(); mockChannel = { subscribe: subscribeSpy, - unsubscribe: unsubscribeSpy, hasSubscribers: false, }; mockNewTracingChannel.mockReturnValue(mockChannel); - plugin = new GoogleGenAIPlugin(); }); afterEach(() => { vi.clearAllMocks(); }); - describe("enable/disable lifecycle", () => { - it("should not subscribe multiple times if enabled twice", () => { - plugin.enable(); - const firstCallCount = subscribeSpy.mock.calls.length; - - plugin.enable(); - const secondCallCount = subscribeSpy.mock.calls.length; - - expect(firstCallCount).toBe(secondCallCount); - }); - - it("should unsubscribe from channels when disabled", () => { - plugin.enable(); - plugin.disable(); - - expect(unsubscribeSpy).toHaveBeenCalled(); - }); - - it("should clear unsubscribers array after disable", () => { - plugin.enable(); - plugin.disable(); - - // Enable again should re-subscribe - subscribeSpy.mockClear(); - plugin.enable(); - - expect(subscribeSpy).toHaveBeenCalled(); - }); - - it("should not crash when disabled without being enabled", () => { - expect(() => plugin.disable()).not.toThrow(); - }); - }); - describe("generateContent channel subscription", () => { it("should extract input correctly", () => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); const subscribeCall = subscribeSpy.mock.calls.find( (call: any) => @@ -199,7 +160,7 @@ describe("GoogleGenAIPlugin", () => { ])( "normalizes $name", ({ usageMetadata, expectedMetrics, absentMetrics }) => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); const handlers = subscribeSpy.mock.calls[0][0]; const event: any = { @@ -227,7 +188,7 @@ describe("GoogleGenAIPlugin", () => { ); it("preserves explicitly reported zero usage", () => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); const handlers = subscribeSpy.mock.calls[0][0]; const event: any = { @@ -275,7 +236,7 @@ describe("GoogleGenAIPlugin", () => { describe("interactions.create channel subscription", () => { it("subscribes to the interactions.create channel", () => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); expect(mockNewTracingChannel).toHaveBeenCalledWith( "orchestrion:@google/genai:interactions.create", @@ -284,7 +245,7 @@ describe("GoogleGenAIPlugin", () => { }); it("logs non-streaming interaction output and metrics", () => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); const handlers = subscribeSpy.mock.calls[3][0]; const scheduledAt = new Date("2026-01-02T03:04:05.000Z"); @@ -412,7 +373,7 @@ describe("GoogleGenAIPlugin", () => { }); it("preserves zero and missing interaction usage values", () => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); const handlers = subscribeSpy.mock.calls[3][0]; const event: any = { @@ -488,7 +449,7 @@ describe("GoogleGenAIPlugin", () => { }); it("does not trace background interaction tasks", () => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); const handlers = subscribeSpy.mock.calls[3][0]; const event: any = { @@ -512,7 +473,7 @@ describe("GoogleGenAIPlugin", () => { }); it("aggregates streaming interaction events when consumed", async () => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); async function* stream() { yield { @@ -599,7 +560,7 @@ describe("GoogleGenAIPlugin", () => { }); it("ends the interaction span when a stream errors", async () => { - plugin.enable(); + registerGoogleGenAIInstrumentation(); const streamError = new Error("stream failed"); async function* stream() { @@ -651,7 +612,7 @@ describe("Google GenAI serialization functions", () => { contents: "Hello world", }; - // Since these are private functions, we'll test them through the plugin behavior + // Since these are private functions, test them through consumer behavior. // For now, we'll verify the structure by examining what gets logged expect(params.model).toBe("gemini-pro"); expect(params.contents).toBe("Hello world"); diff --git a/js/src/instrumentation/plugins/google-genai-plugin.ts b/js/src/instrumentation/providers/google-genai-instrumentation.ts similarity index 92% rename from js/src/instrumentation/plugins/google-genai-plugin.ts rename to js/src/instrumentation/providers/google-genai-instrumentation.ts index fc70e112c..85e5ae049 100644 --- a/js/src/instrumentation/plugins/google-genai-plugin.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.ts @@ -1,5 +1,4 @@ -import { BasePlugin } from "../core"; -import { traceStreamingChannel, unsubscribeAll } from "../core/channel-tracing"; +import { traceStreamingChannel } from "../core/channel-tracing"; import type { ChannelMessage, ErrorOf, @@ -74,29 +73,25 @@ function createWrapperParityEvent(args: { } /** - * Auto-instrumentation plugin for the Google GenAI SDK. + * Internal auto-instrumentation consumer for the Google GenAI SDK. * - * This plugin subscribes to orchestrion channels for Google GenAI SDK methods + * This consumer subscribes to orchestrion channels for Google GenAI SDK methods * and creates Braintrust spans to track: * - models.generateContent (non-streaming) * - models.generateContentStream (streaming) * - models.embedContent (embeddings) * - * The plugin handles: + * The consumer handles: * - Google-specific token metrics (promptTokenCount, candidatesTokenCount, cachedContentTokenCount) * - Processing streaming responses * - Converting inline data (images) to Attachment objects * - Tool calls (functionCall, functionResponse) and executable code results */ -export class GoogleGenAIPlugin extends BasePlugin { - protected onEnable(): void { +class GoogleGenAIInstrumentationConsumer { + public register(): void { this.subscribeToGoogleGenAIChannels(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } - private subscribeToGoogleGenAIChannels(): void { this.subscribeToGenerateContentChannel(); this.subscribeToGenerateContentStreamChannel(); @@ -110,32 +105,28 @@ export class GoogleGenAIPlugin extends BasePlugin { ChannelMessage >; const states = new WeakMap(); - const unbindCurrentSpanStore = bindCurrentSpanStoreToStart( - tracingChannel, - states, - (event) => { - const params = event.arguments[0]; - const input = serializeGenerateContentInput(params); - const metadata = extractGenerateContentMetadata(params); - const span = startBaseSpan( - withSpanInstrumentationName( - { - name: "generate_content", - spanAttributes: { - type: SpanTypeAttribute.LLM, - }, - event: createWrapperParityEvent({ input, metadata }), + bindCurrentSpanStoreToStart(tracingChannel, states, (event) => { + const params = event.arguments[0]; + const input = serializeGenerateContentInput(params); + const metadata = extractGenerateContentMetadata(params); + const span = startBaseSpan( + withSpanInstrumentationName( + { + name: "generate_content", + spanAttributes: { + type: SpanTypeAttribute.LLM, }, - INSTRUMENTATION_NAMES.GOOGLE_GENAI, - ), - ); - - return { - span, - startTime: getCurrentUnixTimestamp(), - }; - }, - ); + event: createWrapperParityEvent({ input, metadata }), + }, + INSTRUMENTATION_NAMES.GOOGLE_GENAI, + ), + ); + + return { + span, + startTime: getCurrentUnixTimestamp(), + }; + }); const handlers: IsoChannelHandlers> = { @@ -192,10 +183,6 @@ export class GoogleGenAIPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }); } private subscribeToGenerateContentStreamChannel(): void { @@ -228,9 +215,6 @@ export class GoogleGenAIPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - tracingChannel.unsubscribe(handlers); - }); } private subscribeToEmbedContentChannel(): void { @@ -239,32 +223,28 @@ export class GoogleGenAIPlugin extends BasePlugin { ChannelMessage >; const states = new WeakMap(); - const unbindCurrentSpanStore = bindCurrentSpanStoreToStart( - tracingChannel, - states, - (event) => { - const params = event.arguments[0]; - const input = serializeEmbedContentInput(params); - const metadata = extractEmbedContentMetadata(params); - const span = startBaseSpan( - withSpanInstrumentationName( - { - name: "embed_content", - spanAttributes: { - type: SpanTypeAttribute.LLM, - }, - event: createWrapperParityEvent({ input, metadata }), + bindCurrentSpanStoreToStart(tracingChannel, states, (event) => { + const params = event.arguments[0]; + const input = serializeEmbedContentInput(params); + const metadata = extractEmbedContentMetadata(params); + const span = startBaseSpan( + withSpanInstrumentationName( + { + name: "embed_content", + spanAttributes: { + type: SpanTypeAttribute.LLM, }, - INSTRUMENTATION_NAMES.GOOGLE_GENAI, - ), - ); - - return { - span, - startTime: getCurrentUnixTimestamp(), - }; - }, - ); + event: createWrapperParityEvent({ input, metadata }), + }, + INSTRUMENTATION_NAMES.GOOGLE_GENAI, + ), + ); + + return { + span, + startTime: getCurrentUnixTimestamp(), + }; + }); const handlers: IsoChannelHandlers> = { start: (event) => { @@ -316,33 +296,26 @@ export class GoogleGenAIPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindCurrentSpanStore?.(); - tracingChannel.unsubscribe(handlers); - }); } private subscribeToInteractionsCreateChannel(): void { - this.unsubscribers.push( - traceStreamingChannel( - googleGenAIChannels.interactionsCreate as InteractionsCreateChannel, - { - name: "create_interaction", - shouldTrace: ([params]) => !isBackgroundInteractionCreate(params), - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => ({ - input: serializeInteractionInput(params), - metadata: extractInteractionMetadata(params), - }), - extractOutput: (result) => serializeInteractionValue(result), - extractMetadata: (result) => - extractInteractionResponseMetadata(result), - extractMetrics: (result, startTime) => - cleanMetrics(extractInteractionMetrics(result, startTime)), - aggregateChunks: (chunks, _result, _event, startTime) => - aggregateInteractionEvents(chunks, startTime), - }, - ), + traceStreamingChannel( + googleGenAIChannels.interactionsCreate as InteractionsCreateChannel, + { + name: "create_interaction", + shouldTrace: ([params]) => !isBackgroundInteractionCreate(params), + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => ({ + input: serializeInteractionInput(params), + metadata: extractInteractionMetadata(params), + }), + extractOutput: (result) => serializeInteractionValue(result), + extractMetadata: (result) => extractInteractionResponseMetadata(result), + extractMetrics: (result, startTime) => + cleanMetrics(extractInteractionMetrics(result, startTime)), + aggregateChunks: (chunks, _result, _event, startTime) => + aggregateInteractionEvents(chunks, startTime), + }, ); } } @@ -372,7 +345,7 @@ function bindCurrentSpanStoreToStart< tracingChannel: IsoTracingChannel>, states: WeakMap, create: (event: StartOf) => SpanState, -): (() => void) | undefined { +): void { const state = _internalGetGlobalState(); const contextManager = state?.contextManager; const startChannel = tracingChannel.start as @@ -381,7 +354,6 @@ function bindCurrentSpanStoreToStart< store: CurrentSpanStore, callback: (event: ChannelMessage) => unknown, ) => void; - unbindStore?: (store: CurrentSpanStore) => void; } & object) | undefined; const currentSpanStore = contextManager @@ -393,7 +365,7 @@ function bindCurrentSpanStoreToStart< : undefined; if (!startChannel?.bindStore || !currentSpanStore) { - return undefined; + return; } startChannel.bindStore(currentSpanStore, (event) => { @@ -402,10 +374,6 @@ function bindCurrentSpanStoreToStart< ).span; return contextManager!.wrapSpanForStore(span); }); - - return () => { - startChannel.unbindStore?.(currentSpanStore); - }; } function logErrorAndEndSpan( @@ -1626,3 +1594,13 @@ function tryToDict(obj: unknown): Record | null { return null; } + +let googleGenAIInstrumentationConsumer: + | GoogleGenAIInstrumentationConsumer + | undefined; + +export function registerGoogleGenAIInstrumentation(): void { + googleGenAIInstrumentationConsumer ??= + new GoogleGenAIInstrumentationConsumer(); + googleGenAIInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/groq-channels.ts b/js/src/instrumentation/providers/groq-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/groq-channels.ts rename to js/src/instrumentation/providers/groq-channels.ts diff --git a/js/src/instrumentation/plugins/groq-plugin.test.ts b/js/src/instrumentation/providers/groq-instrumentation.test.ts similarity index 98% rename from js/src/instrumentation/plugins/groq-plugin.test.ts rename to js/src/instrumentation/providers/groq-instrumentation.test.ts index a140deb80..3b10736d4 100644 --- a/js/src/instrumentation/plugins/groq-plugin.test.ts +++ b/js/src/instrumentation/providers/groq-instrumentation.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { aggregateGroqChatCompletionChunks, parseGroqMetrics, -} from "./groq-plugin"; +} from "./groq-instrumentation"; describe("parseGroqMetrics", () => { it("merges OpenAI-compatible usage metrics with Groq cache metrics", () => { diff --git a/js/src/instrumentation/plugins/groq-plugin.ts b/js/src/instrumentation/providers/groq-instrumentation.ts similarity index 58% rename from js/src/instrumentation/plugins/groq-plugin.ts rename to js/src/instrumentation/providers/groq-instrumentation.ts index 10b6d2c0a..6f2b85d5f 100644 --- a/js/src/instrumentation/plugins/groq-plugin.ts +++ b/js/src/instrumentation/providers/groq-instrumentation.ts @@ -1,8 +1,6 @@ -import { BasePlugin } from "../core"; import { traceAsyncChannel, traceStreamingChannel, - unsubscribeAll, } from "../core/channel-tracing"; import { SpanTypeAttribute } from "../../../util/index"; import { processInputAttachments } from "../../wrappers/attachment-utils"; @@ -10,63 +8,53 @@ import { getCurrentUnixTimestamp } from "../../util"; import { aggregateChatCompletionChunks, parseMetricsFromUsage, -} from "./openai-plugin"; +} from "./openai-instrumentation"; import { groqChannels } from "./groq-channels"; import type { GroqChatCompletion, GroqChatCompletionChunk, } from "../../vendor-sdk-types/groq"; -export class GroqPlugin extends BasePlugin { - protected onEnable(): void { - this.unsubscribers.push( - traceStreamingChannel(groqChannels.chatCompletionsCreate, { - name: "groq.chat.completions.create", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { messages, ...metadata } = params; - return { - input: processInputAttachments(messages), - metadata: { ...metadata, provider: "groq" }, - }; - }, - extractOutput: (result) => result?.choices, - extractMetrics: (result, startTime) => { - const metrics = parseGroqMetrics(result); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - aggregateChunks: aggregateGroqChatCompletionChunks, - }), - ); +export function registerGroqInstrumentation(): void { + traceStreamingChannel(groqChannels.chatCompletionsCreate, { + name: "groq.chat.completions.create", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { messages, ...metadata } = params; + return { + input: processInputAttachments(messages), + metadata: { ...metadata, provider: "groq" }, + }; + }, + extractOutput: (result) => result?.choices, + extractMetrics: (result, startTime) => { + const metrics = parseGroqMetrics(result); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + aggregateChunks: aggregateGroqChatCompletionChunks, + }); - this.unsubscribers.push( - traceAsyncChannel(groqChannels.embeddingsCreate, { - name: "groq.embeddings.create", - type: SpanTypeAttribute.LLM, - extractInput: ([params]) => { - const { input, ...metadata } = params; - return { - input, - metadata: { ...metadata, provider: "groq" }, - }; - }, - extractOutput: (result) => { - const embedding = result?.data?.[0]?.embedding; - return Array.isArray(embedding) - ? { embedding_length: embedding.length } - : undefined; - }, - extractMetrics: (result) => parseGroqMetrics(result), - }), - ); - } - - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } + traceAsyncChannel(groqChannels.embeddingsCreate, { + name: "groq.embeddings.create", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { input, ...metadata } = params; + return { + input, + metadata: { ...metadata, provider: "groq" }, + }; + }, + extractOutput: (result) => { + const embedding = result?.data?.[0]?.embedding; + return Array.isArray(embedding) + ? { embedding_length: embedding.length } + : undefined; + }, + extractMetrics: (result) => parseGroqMetrics(result), + }); } export function parseGroqMetrics( diff --git a/js/src/instrumentation/plugins/harness-agent-context.test.ts b/js/src/instrumentation/providers/harness-agent-context.test.ts similarity index 100% rename from js/src/instrumentation/plugins/harness-agent-context.test.ts rename to js/src/instrumentation/providers/harness-agent-context.test.ts diff --git a/js/src/instrumentation/plugins/huggingface-channels.ts b/js/src/instrumentation/providers/huggingface-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/huggingface-channels.ts rename to js/src/instrumentation/providers/huggingface-channels.ts diff --git a/js/src/instrumentation/plugins/huggingface-plugin.test.ts b/js/src/instrumentation/providers/huggingface-instrumentation.test.ts similarity index 98% rename from js/src/instrumentation/plugins/huggingface-plugin.test.ts rename to js/src/instrumentation/providers/huggingface-instrumentation.test.ts index 8b2b80286..6c55e8bbe 100644 --- a/js/src/instrumentation/plugins/huggingface-plugin.test.ts +++ b/js/src/instrumentation/providers/huggingface-instrumentation.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { aggregateChatCompletionChunks, extractResponseMetadata, -} from "./huggingface-plugin"; +} from "./huggingface-instrumentation"; describe("extractResponseMetadata", () => { it("keeps allowlisted response fields", () => { diff --git a/js/src/instrumentation/plugins/huggingface-plugin.ts b/js/src/instrumentation/providers/huggingface-instrumentation.ts similarity index 87% rename from js/src/instrumentation/plugins/huggingface-plugin.ts rename to js/src/instrumentation/providers/huggingface-instrumentation.ts index 797c3bf82..0407adffa 100644 --- a/js/src/instrumentation/plugins/huggingface-plugin.ts +++ b/js/src/instrumentation/providers/huggingface-instrumentation.ts @@ -1,10 +1,8 @@ import { traceAsyncChannel, traceSyncStreamChannel, - unsubscribeAll, } from "../core/channel-tracing"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; -import { BasePlugin } from "../core"; import { SpanTypeAttribute, isObject } from "../../../util/index"; import { getCurrentUnixTimestamp } from "../../util"; import { parseMetricsFromUsage } from "../../openai-utils"; @@ -41,62 +39,54 @@ const RESPONSE_METADATA_ALLOWLIST = new Set([ "object", ]); -export class HuggingFacePlugin extends BasePlugin { - protected onEnable(): void { - this.unsubscribers.push( - traceAsyncChannel(huggingFaceChannels.chatCompletion, { - name: "huggingface.chat_completion", - type: SpanTypeAttribute.LLM, - extractInput: extractChatInputWithMetadata, - extractOutput: (result) => result?.choices, - extractMetadata: (result) => extractResponseMetadata(result), - extractMetrics: (result) => parseMetricsFromUsage(result?.usage), - }), - traceSyncStreamChannel(huggingFaceChannels.chatCompletionStream, { - name: "huggingface.chat_completion_stream", - type: SpanTypeAttribute.LLM, - extractInput: extractChatInputWithMetadata, - patchResult: ({ result, span, startTime }) => - patchChatCompletionStream({ - result, - span, - startTime, - }), - }), - traceAsyncChannel(huggingFaceChannels.textGeneration, { - name: "huggingface.text_generation", - type: SpanTypeAttribute.LLM, - extractInput: extractTextGenerationInputWithMetadata, - extractOutput: (result) => - isObject(result) ? { generated_text: result.generated_text } : result, - extractMetadata: extractTextGenerationMetadata, - extractMetrics: (result) => - extractTextGenerationMetrics(result?.details ?? null), - }), - traceSyncStreamChannel(huggingFaceChannels.textGenerationStream, { - name: "huggingface.text_generation_stream", - type: SpanTypeAttribute.LLM, - extractInput: extractTextGenerationInputWithMetadata, - patchResult: ({ result, span, startTime }) => - patchTextGenerationStream({ - result, - span, - startTime, - }), +export function registerHuggingFaceInstrumentation(): void { + traceAsyncChannel(huggingFaceChannels.chatCompletion, { + name: "huggingface.chat_completion", + type: SpanTypeAttribute.LLM, + extractInput: extractChatInputWithMetadata, + extractOutput: (result) => result?.choices, + extractMetadata: (result) => extractResponseMetadata(result), + extractMetrics: (result) => parseMetricsFromUsage(result?.usage), + }); + traceSyncStreamChannel(huggingFaceChannels.chatCompletionStream, { + name: "huggingface.chat_completion_stream", + type: SpanTypeAttribute.LLM, + extractInput: extractChatInputWithMetadata, + patchResult: ({ result, span, startTime }) => + patchChatCompletionStream({ + result, + span, + startTime, }), - traceAsyncChannel(huggingFaceChannels.featureExtraction, { - name: "huggingface.feature_extraction", - type: SpanTypeAttribute.LLM, - extractInput: extractFeatureExtractionInputWithMetadata, - extractOutput: summarizeFeatureExtractionOutput, - extractMetrics: () => ({}), + }); + traceAsyncChannel(huggingFaceChannels.textGeneration, { + name: "huggingface.text_generation", + type: SpanTypeAttribute.LLM, + extractInput: extractTextGenerationInputWithMetadata, + extractOutput: (result) => + isObject(result) ? { generated_text: result.generated_text } : result, + extractMetadata: extractTextGenerationMetadata, + extractMetrics: (result) => + extractTextGenerationMetrics(result?.details ?? null), + }); + traceSyncStreamChannel(huggingFaceChannels.textGenerationStream, { + name: "huggingface.text_generation_stream", + type: SpanTypeAttribute.LLM, + extractInput: extractTextGenerationInputWithMetadata, + patchResult: ({ result, span, startTime }) => + patchTextGenerationStream({ + result, + span, + startTime, }), - ); - } - - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } + }); + traceAsyncChannel(huggingFaceChannels.featureExtraction, { + name: "huggingface.feature_extraction", + type: SpanTypeAttribute.LLM, + extractInput: extractFeatureExtractionInputWithMetadata, + extractOutput: summarizeFeatureExtractionOutput, + extractMetrics: () => ({}), + }); } function addProviderMetadata( diff --git a/js/src/instrumentation/plugins/huggingface-transformers-channels.ts b/js/src/instrumentation/providers/huggingface-transformers-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/huggingface-transformers-channels.ts rename to js/src/instrumentation/providers/huggingface-transformers-channels.ts diff --git a/js/src/instrumentation/plugins/huggingface-transformers-plugin.test.ts b/js/src/instrumentation/providers/huggingface-transformers-instrumentation.test.ts similarity index 97% rename from js/src/instrumentation/plugins/huggingface-transformers-plugin.test.ts rename to js/src/instrumentation/providers/huggingface-transformers-instrumentation.test.ts index 667fc19c7..b32e93579 100644 --- a/js/src/instrumentation/plugins/huggingface-transformers-plugin.test.ts +++ b/js/src/instrumentation/providers/huggingface-transformers-instrumentation.test.ts @@ -3,13 +3,13 @@ import { registerHuggingFaceTransformersPipeline, type HuggingFaceTransformersEventContext, } from "./huggingface-transformers-channels"; -import { _exportsForTestingOnly } from "./huggingface-transformers-plugin"; +import { _exportsForTestingOnly } from "./huggingface-transformers-instrumentation"; import type { HuggingFaceTransformersPipeline } from "../../vendor-sdk-types/huggingface-transformers"; const { extractInput, extractMetadata, extractOutput, isSupportedTask } = _exportsForTestingOnly; -describe("HuggingFaceTransformersPlugin extraction", () => { +describe("registerHuggingFaceTransformersInstrumentation extraction", () => { it("normalizes generation and chat payloads", () => { expect(extractInput("text-generation", ["Hello"])).toEqual([ { role: "user", content: "Hello" }, diff --git a/js/src/instrumentation/plugins/huggingface-transformers-plugin.ts b/js/src/instrumentation/providers/huggingface-transformers-instrumentation.ts similarity index 82% rename from js/src/instrumentation/plugins/huggingface-transformers-plugin.ts rename to js/src/instrumentation/providers/huggingface-transformers-instrumentation.ts index 8d41b535a..09e817685 100644 --- a/js/src/instrumentation/plugins/huggingface-transformers-plugin.ts +++ b/js/src/instrumentation/providers/huggingface-transformers-instrumentation.ts @@ -1,5 +1,4 @@ -import { BasePlugin } from "../core"; -import { traceAsyncChannel, unsubscribeAll } from "../core/channel-tracing"; +import { traceAsyncChannel } from "../core/channel-tracing"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { SpanTypeAttribute, isObject } from "../../../util"; @@ -19,43 +18,37 @@ const REQUEST_METADATA_KEYS = [ "stop", ] as const; -export class HuggingFaceTransformersPlugin extends BasePlugin { - protected onEnable(): void { +class HuggingFaceTransformersInstrumentationConsumer { + public register(): void { this.subscribeToPipelineFactory(); - this.unsubscribers.push( - traceAsyncChannel(huggingFaceTransformersChannels.pipelineCall, { - name: (_args, event) => { - const task = getTask(event as HuggingFaceTransformersEventContext); - const operation = task?.replaceAll("-", "_") ?? "unknown"; - return `huggingface.transformers.${operation}`; - }, - type: SpanTypeAttribute.LLM, - shouldTrace: (_args, event) => - isSupportedHuggingFaceTransformersTask( - getTask(event as HuggingFaceTransformersEventContext), - ), - extractInput: (args, event) => ({ - input: extractInput( - getTask(event as HuggingFaceTransformersEventContext), - args, - ), - metadata: extractMetadata( - event as HuggingFaceTransformersEventContext, - args, - ), - }), - extractOutput: (result, event) => - extractOutput( - getTask(event as HuggingFaceTransformersEventContext), - result, - ), - extractMetrics: () => ({}), + traceAsyncChannel(huggingFaceTransformersChannels.pipelineCall, { + name: (_args, event) => { + const task = getTask(event as HuggingFaceTransformersEventContext); + const operation = task?.replaceAll("-", "_") ?? "unknown"; + return `huggingface.transformers.${operation}`; + }, + type: SpanTypeAttribute.LLM, + shouldTrace: (_args, event) => + isSupportedHuggingFaceTransformersTask( + getTask(event as HuggingFaceTransformersEventContext), + ), + extractInput: (args, event) => ({ + input: extractInput( + getTask(event as HuggingFaceTransformersEventContext), + args, + ), + metadata: extractMetadata( + event as HuggingFaceTransformersEventContext, + args, + ), }), - ); - } - - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); + extractOutput: (result, event) => + extractOutput( + getTask(event as HuggingFaceTransformersEventContext), + result, + ), + extractMetrics: () => ({}), + }); } private subscribeToPipelineFactory(): void { @@ -79,7 +72,6 @@ export class HuggingFaceTransformersPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => channel.unsubscribe(handlers)); } } @@ -321,3 +313,13 @@ export const _exportsForTestingOnly = { extractOutput, isSupportedTask: isSupportedHuggingFaceTransformersTask, }; + +let huggingFaceTransformersInstrumentationConsumer: + | HuggingFaceTransformersInstrumentationConsumer + | undefined; + +export function registerHuggingFaceTransformersInstrumentation(): void { + huggingFaceTransformersInstrumentationConsumer ??= + new HuggingFaceTransformersInstrumentationConsumer(); + huggingFaceTransformersInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/instrumentation-names.test.ts b/js/src/instrumentation/providers/instrumentation-names.test.ts similarity index 100% rename from js/src/instrumentation/plugins/instrumentation-names.test.ts rename to js/src/instrumentation/providers/instrumentation-names.test.ts diff --git a/js/src/instrumentation/plugins/langchain-channels.ts b/js/src/instrumentation/providers/langchain-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/langchain-channels.ts rename to js/src/instrumentation/providers/langchain-channels.ts diff --git a/js/src/instrumentation/plugins/langchain-plugin.test.ts b/js/src/instrumentation/providers/langchain-instrumentation.test.ts similarity index 79% rename from js/src/instrumentation/plugins/langchain-plugin.test.ts rename to js/src/instrumentation/providers/langchain-instrumentation.test.ts index 29ace20ab..570972a9d 100644 --- a/js/src/instrumentation/plugins/langchain-plugin.test.ts +++ b/js/src/instrumentation/providers/langchain-instrumentation.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import { LangChainPlugin } from "./langchain-plugin"; +import { beforeAll, describe, expect, it } from "vitest"; +import { registerLangChainInstrumentation } from "./langchain-instrumentation"; import { langChainChannels } from "./langchain-channels"; function createManager(handlers: unknown[] = []) { @@ -35,14 +35,15 @@ function createArgumentsObject(...args: unknown[]): IArguments { })(...args); } -describe("LangChainPlugin", () => { +describe("registerLangChainInstrumentation", () => { + beforeAll(() => { + registerLangChainInstrumentation(); + }); + it("injects a Braintrust callback handler into empty CallbackManager.configure() arguments", () => { - const plugin = new LangChainPlugin(); const args: unknown[] = []; - plugin.enable(); traceConfigureArguments(args); - plugin.disable(); expect(args[0]).toEqual([ expect.objectContaining({ @@ -52,12 +53,9 @@ describe("LangChainPlugin", () => { }); it("injects a Braintrust callback handler into real arguments objects", () => { - const plugin = new LangChainPlugin(); const args = createArgumentsObject(); - plugin.enable(); traceConfigureArgumentsObject(args); - plugin.disable(); expect(args[0]).toEqual([ expect.objectContaining({ @@ -67,12 +65,9 @@ describe("LangChainPlugin", () => { }); it("injects a Braintrust callback handler into CallbackManager.configure() results", () => { - const plugin = new LangChainPlugin(); const manager = createManager(); - plugin.enable(); traceConfigureResult(manager); - plugin.disable(); expect(manager.handlers).toHaveLength(1); expect(manager.handlers[0]).toMatchObject({ @@ -81,37 +76,25 @@ describe("LangChainPlugin", () => { }); it("does not inject duplicate handlers into the same manager", () => { - const plugin = new LangChainPlugin(); const manager = createManager(); - plugin.enable(); traceConfigureResult(manager); traceConfigureResult(manager); - plugin.disable(); expect(manager.handlers).toHaveLength(1); }); it("does not inject when a Braintrust callback handler is already present", () => { - const plugin = new LangChainPlugin(); const existingHandler = { name: "BraintrustCallbackHandler" }; const manager = createManager([existingHandler]); - plugin.enable(); traceConfigureResult(manager); - plugin.disable(); expect(manager.handlers).toEqual([existingHandler]); }); it("gracefully ignores undefined and non-manager results", () => { - const plugin = new LangChainPlugin(); - - plugin.enable(); - expect(() => traceConfigureResult(undefined)).not.toThrow(); expect(() => traceConfigureResult({ handlers: [] })).not.toThrow(); - - plugin.disable(); }); }); diff --git a/js/src/instrumentation/plugins/langchain-plugin.ts b/js/src/instrumentation/providers/langchain-instrumentation.ts similarity index 89% rename from js/src/instrumentation/plugins/langchain-plugin.ts rename to js/src/instrumentation/providers/langchain-instrumentation.ts index 85ef66083..42a5f7fd5 100644 --- a/js/src/instrumentation/plugins/langchain-plugin.ts +++ b/js/src/instrumentation/providers/langchain-instrumentation.ts @@ -1,4 +1,3 @@ -import { BasePlugin } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import type { LangChainCallbackManager } from "../../vendor-sdk-types/langchain"; @@ -12,22 +11,14 @@ type LangChainConfigureChannel = | typeof langChainChannels.configure | typeof langChainChannels.configureSync; -export class LangChainPlugin extends BasePlugin { +class LangChainInstrumentationConsumer { private injectedManagers = new WeakSet(); - protected onEnable(): void { + public register(): void { this.subscribeToConfigure(langChainChannels.configure); this.subscribeToConfigure(langChainChannels.configureSync); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - this.injectedManagers = new WeakSet(); - } - private subscribeToConfigure(channel: LangChainConfigureChannel): void { const tracingChannel: IsoTracingChannel< ChannelMessage @@ -45,9 +36,6 @@ export class LangChainPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - tracingChannel.unsubscribe(handlers); - }); } private injectHandler(result: unknown): void { @@ -126,3 +114,12 @@ function isBraintrustHandler(handler: unknown): boolean { Reflect.get(handler, "name") === BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME ); } + +let langChainInstrumentationConsumer: + | LangChainInstrumentationConsumer + | undefined; + +export function registerLangChainInstrumentation(): void { + langChainInstrumentationConsumer ??= new LangChainInstrumentationConsumer(); + langChainInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/langsmith-channels.ts b/js/src/instrumentation/providers/langsmith-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/langsmith-channels.ts rename to js/src/instrumentation/providers/langsmith-channels.ts diff --git a/js/src/instrumentation/plugins/langsmith-plugin.test.ts b/js/src/instrumentation/providers/langsmith-instrumentation.test.ts similarity index 88% rename from js/src/instrumentation/plugins/langsmith-plugin.test.ts rename to js/src/instrumentation/providers/langsmith-instrumentation.test.ts index 8e9b3aed4..55a986a74 100644 --- a/js/src/instrumentation/plugins/langsmith-plugin.test.ts +++ b/js/src/instrumentation/providers/langsmith-instrumentation.test.ts @@ -1,37 +1,36 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { configureNode } from "../../node/config"; import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { configureInstrumentation } from "../registry"; import { langSmithChannels } from "./langsmith-channels"; -import { LangSmithPlugin } from "./langsmith-plugin"; +import { registerLangSmithInstrumentation } from "./langsmith-instrumentation"; try { + configureInstrumentation({ integrations: { langsmith: false } }); configureNode(); } catch { // Best-effort initialization for test environments. } -describe("LangSmithPlugin", () => { +describe("registerLangSmithInstrumentation", () => { let backgroundLogger: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; - let plugin: LangSmithPlugin; beforeAll(async () => { await _exportsForTestingOnly.simulateLoginForTests(); + registerLangSmithInstrumentation(); }); beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "langsmith-plugin.test.ts", + projectName: "langsmith-instrumentation.test.ts", projectId: "test-project-id", }); - plugin = new LangSmithPlugin(); - plugin.enable(); }); afterEach(() => { - plugin.disable(); _exportsForTestingOnly.clearTestBackgroundLogger(); }); @@ -269,7 +268,7 @@ describe("LangSmithPlugin", () => { expect(await backgroundLogger.drain()).toEqual([]); }); - it("suppresses LangChain-serialized runs only when configured", async () => { + it("suppresses LangChain-serialized runs by default", async () => { const run = { id: "44444444-4444-4444-8444-444444444444", trace_id: "44444444-4444-4444-8444-444444444444", @@ -282,31 +281,5 @@ describe("LangSmithPlugin", () => { arguments: [run.id, run], }); expect(await backgroundLogger.drain()).toEqual([]); - - plugin.disable(); - plugin = new LangSmithPlugin({ skipLangChainRuns: false }); - plugin.enable(); - await langSmithChannels.updateRun.tracePromise(async () => undefined, { - arguments: [run.id, run], - }); - expect(await backgroundLogger.drain()).toHaveLength(1); - }); - - it("bounds the completed-run deduplication cache", () => { - const completedRuns = ( - plugin as unknown as { - completedRuns: { - get(key: string): true | undefined; - set(key: string, value: true): void; - }; - } - ).completedRuns; - - for (let index = 0; index <= 10_000; index++) { - completedRuns.set(`run-${index}`, true); - } - - expect(completedRuns.get("run-0")).toBeUndefined(); - expect(completedRuns.get("run-10000")).toBe(true); }); }); diff --git a/js/src/instrumentation/plugins/langsmith-plugin.ts b/js/src/instrumentation/providers/langsmith-instrumentation.ts similarity index 95% rename from js/src/instrumentation/plugins/langsmith-plugin.ts rename to js/src/instrumentation/providers/langsmith-instrumentation.ts index 5027fa694..1716b88cb 100644 --- a/js/src/instrumentation/plugins/langsmith-plugin.ts +++ b/js/src/instrumentation/providers/langsmith-instrumentation.ts @@ -11,8 +11,6 @@ import type { LangSmithBatchIngestRuns, LangSmithRun, } from "../../vendor-sdk-types/langsmith"; -import { BasePlugin } from "../core"; -import { unsubscribeAll } from "../core/channel-tracing"; import type { ChannelMessage } from "../core/channel-definitions"; import { langSmithChannels } from "./langsmith-channels"; @@ -21,7 +19,7 @@ type ActiveRun = { span: Span; }; -type LangSmithPluginOptions = { +type LangSmithInstrumentationOptions = { skipLangChainRuns?: boolean; }; @@ -43,19 +41,18 @@ const LLM_SETTING_KEYS = [ "response_format", ] as const; -export class LangSmithPlugin extends BasePlugin { +class LangSmithInstrumentationConsumer { private readonly activeRuns = new Map(); private readonly completedRuns = new LRUCache({ max: MAX_COMPLETED_RUNS, }); private readonly skipLangChainRuns: boolean; - constructor(options: LangSmithPluginOptions = {}) { - super(); + constructor(options: LangSmithInstrumentationOptions = {}) { this.skipLangChainRuns = options.skipLangChainRuns ?? true; } - protected onEnable(): void { + public register(): void { const createChannel = langSmithChannels.createRun.tracingChannel(); const createHandlers = { start: ( @@ -67,7 +64,6 @@ export class LangSmithPlugin extends BasePlugin { }, }; createChannel.subscribe(createHandlers); - this.unsubscribers.push(() => createChannel.unsubscribe(createHandlers)); const updateChannel = langSmithChannels.updateRun.tracingChannel(); const updateHandlers = { @@ -80,7 +76,6 @@ export class LangSmithPlugin extends BasePlugin { }, }; updateChannel.subscribe(updateHandlers); - this.unsubscribers.push(() => updateChannel.unsubscribe(updateHandlers)); const batchChannel = langSmithChannels.batchIngestRuns.tracingChannel(); const batchHandlers = { @@ -93,16 +88,6 @@ export class LangSmithPlugin extends BasePlugin { }, }; batchChannel.subscribe(batchHandlers); - this.unsubscribers.push(() => batchChannel.unsubscribe(batchHandlers)); - } - - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - for (const { span } of this.activeRuns.values()) { - span.end(); - } - this.activeRuns.clear(); - this.completedRuns.clear(); } private processBatch(batch: LangSmithBatchIngestRuns): void { @@ -337,6 +322,19 @@ function mergeRuns( return Object.fromEntries(entries); } +let langSmithInstrumentationConsumer: + | LangSmithInstrumentationConsumer + | undefined; + +export function registerLangSmithInstrumentation( + options: LangSmithInstrumentationOptions = {}, +): void { + langSmithInstrumentationConsumer ??= new LangSmithInstrumentationConsumer( + options, + ); + langSmithInstrumentationConsumer.register(); +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/js/src/instrumentation/plugins/mistral-channels.ts b/js/src/instrumentation/providers/mistral-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/mistral-channels.ts rename to js/src/instrumentation/providers/mistral-channels.ts diff --git a/js/src/instrumentation/plugins/mistral-plugin.test.ts b/js/src/instrumentation/providers/mistral-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/mistral-plugin.test.ts rename to js/src/instrumentation/providers/mistral-instrumentation.test.ts index 96d410440..adab7b87b 100644 --- a/js/src/instrumentation/plugins/mistral-plugin.test.ts +++ b/js/src/instrumentation/providers/mistral-instrumentation.test.ts @@ -4,7 +4,7 @@ import { extractMistralRequestMetadata, extractMistralResponseMetadata, parseMistralMetricsFromUsage, -} from "./mistral-plugin"; +} from "./mistral-instrumentation"; describe("extractMistralRequestMetadata", () => { it("keeps only allowlisted request metadata", () => { diff --git a/js/src/instrumentation/plugins/mistral-plugin.ts b/js/src/instrumentation/providers/mistral-instrumentation.ts similarity index 77% rename from js/src/instrumentation/plugins/mistral-plugin.ts rename to js/src/instrumentation/providers/mistral-instrumentation.ts index f33f82554..7179b569d 100644 --- a/js/src/instrumentation/plugins/mistral-plugin.ts +++ b/js/src/instrumentation/providers/mistral-instrumentation.ts @@ -1,8 +1,6 @@ -import { BasePlugin } from "../core"; import { traceAsyncChannel, traceStreamingChannel, - unsubscribeAll, } from "../core/channel-tracing"; import { SpanTypeAttribute, isObject } from "../../../util/index"; import { processInputAttachments } from "../../wrappers/attachment-utils"; @@ -19,156 +17,130 @@ import type { MistralToolCallDelta, } from "../../vendor-sdk-types/mistral"; -export class MistralPlugin extends BasePlugin { - protected onEnable(): void { +class MistralInstrumentationConsumer { + public register(): void { this.subscribeToMistralChannels(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } - private subscribeToMistralChannels(): void { - this.unsubscribers.push( - traceStreamingChannel(mistralChannels.chatComplete, { - name: "mistral.chat.complete", - type: SpanTypeAttribute.LLM, - extractInput: extractMessagesInputWithMetadata, - extractOutput: (result) => { - return result?.choices; - }, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result, startTime) => - extractMistralMetrics(result?.usage, startTime), - }), - ); - - this.unsubscribers.push( - traceStreamingChannel(mistralChannels.chatStream, { - name: "mistral.chat.stream", - type: SpanTypeAttribute.LLM, - extractInput: extractMessagesInputWithMetadata, - extractOutput: extractMistralStreamOutput, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result, startTime) => - extractMistralStreamingMetrics(result, startTime), - aggregateChunks: aggregateMistralStreamChunks, - }), - ); - - this.unsubscribers.push( - traceAsyncChannel(mistralChannels.embeddingsCreate, { - name: "mistral.embeddings.create", - type: SpanTypeAttribute.LLM, - extractInput: extractEmbeddingInputWithMetadata, - extractOutput: (result) => { - const embedding = result?.data?.[0]?.embedding; - return Array.isArray(embedding) - ? { embedding_length: embedding.length } - : undefined; - }, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), - }), - ); - - this.unsubscribers.push( - traceAsyncChannel(mistralChannels.classifiersModerate, { - name: "mistral.classifiers.moderate", - type: SpanTypeAttribute.LLM, - extractInput: extractClassifierInputWithMetadata, - extractOutput: extractClassifierOutput, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), - }), - ); - - this.unsubscribers.push( - traceAsyncChannel(mistralChannels.classifiersModerateChat, { - name: "mistral.classifiers.moderateChat", - type: SpanTypeAttribute.LLM, - extractInput: extractClassifierInputWithMetadata, - extractOutput: extractClassifierOutput, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), - }), - ); - - this.unsubscribers.push( - traceAsyncChannel(mistralChannels.classifiersClassify, { - name: "mistral.classifiers.classify", - type: SpanTypeAttribute.LLM, - extractInput: extractClassifierInputWithMetadata, - extractOutput: extractClassifierOutput, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), - }), - ); - - this.unsubscribers.push( - traceAsyncChannel(mistralChannels.classifiersClassifyChat, { - name: "mistral.classifiers.classifyChat", - type: SpanTypeAttribute.LLM, - extractInput: extractClassifierInputWithMetadata, - extractOutput: extractClassifierOutput, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), - }), - ); - - this.unsubscribers.push( - traceStreamingChannel(mistralChannels.fimComplete, { - name: "mistral.fim.complete", - type: SpanTypeAttribute.LLM, - extractInput: extractPromptInputWithMetadata, - extractOutput: (result) => { - return result?.choices; - }, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result, startTime) => - extractMistralMetrics(result?.usage, startTime), - }), - ); - - this.unsubscribers.push( - traceStreamingChannel(mistralChannels.fimStream, { - name: "mistral.fim.stream", - type: SpanTypeAttribute.LLM, - extractInput: extractPromptInputWithMetadata, - extractOutput: extractMistralStreamOutput, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result, startTime) => - extractMistralStreamingMetrics(result, startTime), - aggregateChunks: aggregateMistralStreamChunks, - }), - ); - - this.unsubscribers.push( - traceStreamingChannel(mistralChannels.agentsComplete, { - name: "mistral.agents.complete", - type: SpanTypeAttribute.LLM, - extractInput: extractMessagesInputWithMetadata, - extractOutput: (result) => { - return result?.choices; - }, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result, startTime) => - extractMistralMetrics(result?.usage, startTime), - }), - ); - - this.unsubscribers.push( - traceStreamingChannel(mistralChannels.agentsStream, { - name: "mistral.agents.stream", - type: SpanTypeAttribute.LLM, - extractInput: extractMessagesInputWithMetadata, - extractOutput: extractMistralStreamOutput, - extractMetadata: (result) => extractMistralResponseMetadata(result), - extractMetrics: (result, startTime) => - extractMistralStreamingMetrics(result, startTime), - aggregateChunks: aggregateMistralStreamChunks, - }), - ); + traceStreamingChannel(mistralChannels.chatComplete, { + name: "mistral.chat.complete", + type: SpanTypeAttribute.LLM, + extractInput: extractMessagesInputWithMetadata, + extractOutput: (result) => { + return result?.choices; + }, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result, startTime) => + extractMistralMetrics(result?.usage, startTime), + }); + + traceStreamingChannel(mistralChannels.chatStream, { + name: "mistral.chat.stream", + type: SpanTypeAttribute.LLM, + extractInput: extractMessagesInputWithMetadata, + extractOutput: extractMistralStreamOutput, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result, startTime) => + extractMistralStreamingMetrics(result, startTime), + aggregateChunks: aggregateMistralStreamChunks, + }); + + traceAsyncChannel(mistralChannels.embeddingsCreate, { + name: "mistral.embeddings.create", + type: SpanTypeAttribute.LLM, + extractInput: extractEmbeddingInputWithMetadata, + extractOutput: (result) => { + const embedding = result?.data?.[0]?.embedding; + return Array.isArray(embedding) + ? { embedding_length: embedding.length } + : undefined; + }, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), + }); + + traceAsyncChannel(mistralChannels.classifiersModerate, { + name: "mistral.classifiers.moderate", + type: SpanTypeAttribute.LLM, + extractInput: extractClassifierInputWithMetadata, + extractOutput: extractClassifierOutput, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), + }); + + traceAsyncChannel(mistralChannels.classifiersModerateChat, { + name: "mistral.classifiers.moderateChat", + type: SpanTypeAttribute.LLM, + extractInput: extractClassifierInputWithMetadata, + extractOutput: extractClassifierOutput, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), + }); + + traceAsyncChannel(mistralChannels.classifiersClassify, { + name: "mistral.classifiers.classify", + type: SpanTypeAttribute.LLM, + extractInput: extractClassifierInputWithMetadata, + extractOutput: extractClassifierOutput, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), + }); + + traceAsyncChannel(mistralChannels.classifiersClassifyChat, { + name: "mistral.classifiers.classifyChat", + type: SpanTypeAttribute.LLM, + extractInput: extractClassifierInputWithMetadata, + extractOutput: extractClassifierOutput, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result) => parseMistralMetricsFromUsage(result?.usage), + }); + + traceStreamingChannel(mistralChannels.fimComplete, { + name: "mistral.fim.complete", + type: SpanTypeAttribute.LLM, + extractInput: extractPromptInputWithMetadata, + extractOutput: (result) => { + return result?.choices; + }, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result, startTime) => + extractMistralMetrics(result?.usage, startTime), + }); + + traceStreamingChannel(mistralChannels.fimStream, { + name: "mistral.fim.stream", + type: SpanTypeAttribute.LLM, + extractInput: extractPromptInputWithMetadata, + extractOutput: extractMistralStreamOutput, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result, startTime) => + extractMistralStreamingMetrics(result, startTime), + aggregateChunks: aggregateMistralStreamChunks, + }); + + traceStreamingChannel(mistralChannels.agentsComplete, { + name: "mistral.agents.complete", + type: SpanTypeAttribute.LLM, + extractInput: extractMessagesInputWithMetadata, + extractOutput: (result) => { + return result?.choices; + }, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result, startTime) => + extractMistralMetrics(result?.usage, startTime), + }); + + traceStreamingChannel(mistralChannels.agentsStream, { + name: "mistral.agents.stream", + type: SpanTypeAttribute.LLM, + extractInput: extractMessagesInputWithMetadata, + extractOutput: extractMistralStreamOutput, + extractMetadata: (result) => extractMistralResponseMetadata(result), + extractMetrics: (result, startTime) => + extractMistralStreamingMetrics(result, startTime), + aggregateChunks: aggregateMistralStreamChunks, + }); } } @@ -909,3 +881,10 @@ export function aggregateMistralStreamChunks( ...(metadata ? { metadata } : {}), }; } + +let mistralInstrumentationConsumer: MistralInstrumentationConsumer | undefined; + +export function registerMistralInstrumentation(): void { + mistralInstrumentationConsumer ??= new MistralInstrumentationConsumer(); + mistralInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/ollama-channels.ts b/js/src/instrumentation/providers/ollama-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/ollama-channels.ts rename to js/src/instrumentation/providers/ollama-channels.ts diff --git a/js/src/instrumentation/plugins/ollama-plugin.test.ts b/js/src/instrumentation/providers/ollama-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/ollama-plugin.test.ts rename to js/src/instrumentation/providers/ollama-instrumentation.test.ts index 0006efea8..4503d4ddf 100644 --- a/js/src/instrumentation/plugins/ollama-plugin.test.ts +++ b/js/src/instrumentation/providers/ollama-instrumentation.test.ts @@ -9,7 +9,7 @@ import { extractOllamaEmbedOutput, extractOllamaGenerateInput, extractOllamaMetrics, -} from "./ollama-plugin"; +} from "./ollama-instrumentation"; configureNode(); diff --git a/js/src/instrumentation/plugins/ollama-plugin.ts b/js/src/instrumentation/providers/ollama-instrumentation.ts similarity index 91% rename from js/src/instrumentation/plugins/ollama-plugin.ts rename to js/src/instrumentation/providers/ollama-instrumentation.ts index f48040348..405b3dff9 100644 --- a/js/src/instrumentation/plugins/ollama-plugin.ts +++ b/js/src/instrumentation/providers/ollama-instrumentation.ts @@ -14,54 +14,44 @@ import type { OllamaToolCall, OllamaUsageResponse, } from "../../vendor-sdk-types/ollama"; -import { BasePlugin } from "../core"; import type { AsyncEndOf } from "../core/channel-definitions"; import { traceAsyncChannel, traceStreamingChannel, - unsubscribeAll, } from "../core/channel-tracing"; import { ollamaChannels } from "./ollama-channels"; -export class OllamaPlugin extends BasePlugin { - protected onEnable(): void { - this.unsubscribers.push( - traceStreamingChannel(ollamaChannels.chat, { - name: "ollama.chat", - type: SpanTypeAttribute.LLM, - extractInput: extractOllamaChatInput, - extractOutput: (result, event) => - extractOllamaChatOutput( - result, - countOllamaToolCalls(event?.arguments?.[0]?.messages), - ), - extractMetadata: extractOllamaResponseMetadata, - extractMetrics: extractOllamaMetrics, - aggregateChunks: aggregateOllamaChatChunks, - }), - traceStreamingChannel(ollamaChannels.generate, { - name: "ollama.generate", - type: SpanTypeAttribute.LLM, - extractInput: extractOllamaGenerateInput, - extractOutput: extractOllamaGenerateOutput, - extractMetadata: extractOllamaResponseMetadata, - extractMetrics: extractOllamaMetrics, - aggregateChunks: aggregateOllamaGenerateChunks, - }), - traceAsyncChannel(ollamaChannels.embed, { - name: "ollama.embed", - type: SpanTypeAttribute.LLM, - extractInput: extractOllamaEmbedInput, - extractOutput: extractOllamaEmbedOutput, - extractMetadata: extractOllamaResponseMetadata, - extractMetrics: extractOllamaMetrics, - }), - ); - } - - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } +export function registerOllamaInstrumentation(): void { + traceStreamingChannel(ollamaChannels.chat, { + name: "ollama.chat", + type: SpanTypeAttribute.LLM, + extractInput: extractOllamaChatInput, + extractOutput: (result, event) => + extractOllamaChatOutput( + result, + countOllamaToolCalls(event?.arguments?.[0]?.messages), + ), + extractMetadata: extractOllamaResponseMetadata, + extractMetrics: extractOllamaMetrics, + aggregateChunks: aggregateOllamaChatChunks, + }); + traceStreamingChannel(ollamaChannels.generate, { + name: "ollama.generate", + type: SpanTypeAttribute.LLM, + extractInput: extractOllamaGenerateInput, + extractOutput: extractOllamaGenerateOutput, + extractMetadata: extractOllamaResponseMetadata, + extractMetrics: extractOllamaMetrics, + aggregateChunks: aggregateOllamaGenerateChunks, + }); + traceAsyncChannel(ollamaChannels.embed, { + name: "ollama.embed", + type: SpanTypeAttribute.LLM, + extractInput: extractOllamaEmbedInput, + extractOutput: extractOllamaEmbedOutput, + extractMetadata: extractOllamaResponseMetadata, + extractMetrics: extractOllamaMetrics, + }); } function isNonNegativeNumber(value: unknown): value is number { diff --git a/js/src/instrumentation/plugins/openai-agents-channels.ts b/js/src/instrumentation/providers/openai-agents-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/openai-agents-channels.ts rename to js/src/instrumentation/providers/openai-agents-channels.ts diff --git a/js/src/instrumentation/plugins/openai-agents-plugin.test.ts b/js/src/instrumentation/providers/openai-agents-instrumentation.test.ts similarity index 88% rename from js/src/instrumentation/plugins/openai-agents-plugin.test.ts rename to js/src/instrumentation/providers/openai-agents-instrumentation.test.ts index 9f2a604bd..fb6f62fba 100644 --- a/js/src/instrumentation/plugins/openai-agents-plugin.test.ts +++ b/js/src/instrumentation/providers/openai-agents-instrumentation.test.ts @@ -1,37 +1,36 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { configureNode } from "../../node/config"; import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { configureInstrumentation } from "../registry"; import { openAIAgentsCoreChannels } from "./openai-agents-channels"; -import { OpenAIAgentsPlugin } from "./openai-agents-plugin"; +import { registerOpenAIAgentsInstrumentation } from "./openai-agents-instrumentation"; try { + configureInstrumentation({ integrations: { openAIAgents: false } }); configureNode(); } catch { // Best-effort initialization for test environments. } -describe("OpenAIAgentsPlugin", () => { +describe("registerOpenAIAgentsInstrumentation", () => { let backgroundLogger: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; - let plugin: OpenAIAgentsPlugin; beforeAll(async () => { await _exportsForTestingOnly.simulateLoginForTests(); + registerOpenAIAgentsInstrumentation(); }); beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "openai-agents-plugin.test.ts", + projectName: "openai-agents-instrumentation.test.ts", projectId: "test-project-id", }); - plugin = new OpenAIAgentsPlugin(); - plugin.enable(); }); afterEach(() => { - plugin.disable(); _exportsForTestingOnly.clearTestBackgroundLogger(); }); diff --git a/js/src/instrumentation/plugins/openai-agents-plugin.ts b/js/src/instrumentation/providers/openai-agents-instrumentation.ts similarity index 80% rename from js/src/instrumentation/plugins/openai-agents-plugin.ts rename to js/src/instrumentation/providers/openai-agents-instrumentation.ts index 4cf8af1a0..9db90ac4c 100644 --- a/js/src/instrumentation/plugins/openai-agents-plugin.ts +++ b/js/src/instrumentation/providers/openai-agents-instrumentation.ts @@ -1,5 +1,3 @@ -import { BasePlugin } from "../core"; -import { unsubscribeAll } from "../core/channel-tracing"; import { isObject } from "../../../util/index"; import { openAIAgentsCoreChannels } from "./openai-agents-channels"; import { OpenAIAgentsTraceProcessor } from "./openai-agents-trace-processor"; @@ -41,18 +39,13 @@ function isOpenAIAgentsSpan(value: unknown): value is OpenAIAgentsSpan { ); } -export class OpenAIAgentsPlugin extends BasePlugin { +class OpenAIAgentsInstrumentationConsumer { private processor = new OpenAIAgentsTraceProcessor(); - protected onEnable(): void { + public register(): void { this.subscribeToTraceLifecycle(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - void this.processor.shutdown(); - } - private subscribeToTraceLifecycle(): void { const traceStartChannel = openAIAgentsCoreChannels.onTraceStart.tracingChannel(); @@ -65,9 +58,6 @@ export class OpenAIAgentsPlugin extends BasePlugin { }, }; traceStartChannel.subscribe(traceStartHandlers); - this.unsubscribers.push(() => - traceStartChannel.unsubscribe(traceStartHandlers), - ); const traceEndChannel = openAIAgentsCoreChannels.onTraceEnd.tracingChannel(); @@ -80,9 +70,6 @@ export class OpenAIAgentsPlugin extends BasePlugin { }, }; traceEndChannel.subscribe(traceEndHandlers); - this.unsubscribers.push(() => - traceEndChannel.unsubscribe(traceEndHandlers), - ); const spanStartChannel = openAIAgentsCoreChannels.onSpanStart.tracingChannel(); @@ -95,9 +82,6 @@ export class OpenAIAgentsPlugin extends BasePlugin { }, }; spanStartChannel.subscribe(spanStartHandlers); - this.unsubscribers.push(() => - spanStartChannel.unsubscribe(spanStartHandlers), - ); const spanEndChannel = openAIAgentsCoreChannels.onSpanEnd.tracingChannel(); const spanEndHandlers = { @@ -109,6 +93,15 @@ export class OpenAIAgentsPlugin extends BasePlugin { }, }; spanEndChannel.subscribe(spanEndHandlers); - this.unsubscribers.push(() => spanEndChannel.unsubscribe(spanEndHandlers)); } } + +let openAIAgentsInstrumentationConsumer: + | OpenAIAgentsInstrumentationConsumer + | undefined; + +export function registerOpenAIAgentsInstrumentation(): void { + openAIAgentsInstrumentationConsumer ??= + new OpenAIAgentsInstrumentationConsumer(); + openAIAgentsInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/openai-agents-trace-processor.ts b/js/src/instrumentation/providers/openai-agents-trace-processor.ts similarity index 100% rename from js/src/instrumentation/plugins/openai-agents-trace-processor.ts rename to js/src/instrumentation/providers/openai-agents-trace-processor.ts diff --git a/js/src/instrumentation/plugins/openai-channels.ts b/js/src/instrumentation/providers/openai-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/openai-channels.ts rename to js/src/instrumentation/providers/openai-channels.ts diff --git a/js/src/instrumentation/plugins/openai-codex-channels.ts b/js/src/instrumentation/providers/openai-codex-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/openai-codex-channels.ts rename to js/src/instrumentation/providers/openai-codex-channels.ts diff --git a/js/src/instrumentation/plugins/openai-codex-plugin.test.ts b/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts similarity index 92% rename from js/src/instrumentation/plugins/openai-codex-plugin.test.ts rename to js/src/instrumentation/providers/openai-codex-instrumentation.test.ts index 932abe0ba..f7250bb3b 100644 --- a/js/src/instrumentation/plugins/openai-codex-plugin.test.ts +++ b/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts @@ -15,11 +15,11 @@ vi.mock("../../logger", () => ({ })); import iso from "../../isomorph"; -import { OpenAICodexPlugin } from "./openai-codex-plugin"; +import { registerOpenAICodexInstrumentation } from "./openai-codex-instrumentation"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; -describe("OpenAICodexPlugin", () => { +describe("registerOpenAICodexInstrumentation", () => { let handlersByName: Map; let spans: Array<{ end: ReturnType; @@ -33,7 +33,6 @@ describe("OpenAICodexPlugin", () => { spans = []; mockNewTracingChannel.mockImplementation((name: string) => ({ subscribe: vi.fn((handlers) => handlersByName.set(name, handlers)), - unsubscribe: vi.fn(), })); mockStartSpan.mockImplementation((args: any) => { const span = { @@ -55,8 +54,7 @@ describe("OpenAICodexPlugin", () => { }); it("uses explicit token totals by precedence instead of summing breakdowns", async () => { - const plugin = new OpenAICodexPlugin(); - plugin.enable(); + registerOpenAICodexInstrumentation(); const runHandlers = handlersByName.get( "orchestrion:@openai/codex-sdk:Thread.run", diff --git a/js/src/instrumentation/plugins/openai-codex-plugin.ts b/js/src/instrumentation/providers/openai-codex-instrumentation.ts similarity index 98% rename from js/src/instrumentation/plugins/openai-codex-plugin.ts rename to js/src/instrumentation/providers/openai-codex-instrumentation.ts index 4005ecb86..abf442120 100644 --- a/js/src/instrumentation/plugins/openai-codex-plugin.ts +++ b/js/src/instrumentation/providers/openai-codex-instrumentation.ts @@ -1,4 +1,4 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; @@ -55,19 +55,12 @@ const PATCHED_STREAMED_TURN = Symbol.for( "braintrust.openai-codex.patched-streamed-turn", ); -export class OpenAICodexPlugin extends BasePlugin { - protected onEnable(): void { +class OpenAICodexInstrumentationConsumer { + public register(): void { this.subscribeToRun(); this.subscribeToRunStreamed(); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - } - private subscribeToRun(): void { const channel = openAICodexChannels.run.tracingChannel(); const states = new WeakMap(); @@ -97,9 +90,6 @@ export class OpenAICodexPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => { - channel.unsubscribe(handlers); - }); } private subscribeToRunStreamed(): void { @@ -131,9 +121,6 @@ export class OpenAICodexPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => { - channel.unsubscribe(handlers); - }); } } @@ -941,3 +928,13 @@ function safeLog(span: Span, event: Parameters[0]): void { function logInstrumentationError(context: string, error: unknown): void { debugLogger.error(`Error processing ${context}:`, error); } + +let openAICodexInstrumentationConsumer: + | OpenAICodexInstrumentationConsumer + | undefined; + +export function registerOpenAICodexInstrumentation(): void { + openAICodexInstrumentationConsumer ??= + new OpenAICodexInstrumentationConsumer(); + openAICodexInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/openai-plugin.test.ts b/js/src/instrumentation/providers/openai-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/openai-plugin.test.ts rename to js/src/instrumentation/providers/openai-instrumentation.test.ts index 5f2b237fa..d51301e85 100644 --- a/js/src/instrumentation/plugins/openai-plugin.test.ts +++ b/js/src/instrumentation/providers/openai-instrumentation.test.ts @@ -3,7 +3,7 @@ import { parseMetricsFromUsage, processImagesInOutput, aggregateChatCompletionChunks, -} from "./openai-plugin"; +} from "./openai-instrumentation"; import { Attachment } from "../../logger"; describe("parseMetricsFromUsage", () => { diff --git a/js/src/instrumentation/providers/openai-instrumentation.ts b/js/src/instrumentation/providers/openai-instrumentation.ts new file mode 100644 index 000000000..30cddb5c2 --- /dev/null +++ b/js/src/instrumentation/providers/openai-instrumentation.ts @@ -0,0 +1,573 @@ +import { + traceAsyncChannel, + traceStreamingChannel, + traceSyncStreamChannel, +} from "../core/channel-tracing"; +import { Attachment } from "../../logger"; +import { SpanTypeAttribute, isObject } from "../../../util/index"; +import { getCurrentUnixTimestamp } from "../../util"; +import { processInputAttachments } from "../../wrappers/attachment-utils"; +import { openAIChannels } from "./openai-channels"; +import { + BRAINTRUST_CACHED_STREAM_METRIC, + getCachedMetricFromHeaders, + parseMetricsFromUsage, +} from "../../openai-utils"; +import type { + OpenAIChatChoice, + OpenAIChatCompletionChunk, + OpenAIChatLogprobs, + OpenAIResponseStreamEvent, +} from "../../vendor-sdk-types/openai"; + +export function registerOpenAIInstrumentation(): void { + // Chat Completions - supports streaming + traceStreamingChannel(openAIChannels.chatCompletionsCreate, { + name: "Chat Completion", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { messages, ...metadata } = params; + return { + input: processInputAttachments(messages), + metadata: { ...metadata, provider: "openai" }, + }; + }, + extractOutput: (result) => { + return result?.choices; + }, + extractMetrics: (result, startTime, endEvent) => { + const metrics = withCachedMetric( + parseMetricsFromUsage(result?.usage), + result, + endEvent, + ); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + aggregateChunks: aggregateChatCompletionChunks, + }); + + // Embeddings + traceAsyncChannel(openAIChannels.embeddingsCreate, { + name: "Embedding", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { input, ...metadata } = params; + return { + input, + metadata: { ...metadata, provider: "openai" }, + }; + }, + extractOutput: (result) => { + const embedding = result?.data?.[0]?.embedding; + return Array.isArray(embedding) + ? { embedding_length: embedding.length } + : undefined; + }, + extractMetrics: (result, _startTime, endEvent) => { + return withCachedMetric( + parseMetricsFromUsage(result?.usage), + result, + endEvent, + ); + }, + }); + + // Beta Chat Completions Parse + traceStreamingChannel(openAIChannels.betaChatCompletionsParse, { + name: "Chat Completion", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { messages, ...metadata } = params; + return { + input: processInputAttachments(messages), + metadata: { ...metadata, provider: "openai" }, + }; + }, + extractOutput: (result) => { + return result?.choices; + }, + extractMetrics: (result, startTime, endEvent) => { + const metrics = withCachedMetric( + parseMetricsFromUsage(result?.usage), + result, + endEvent, + ); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + aggregateChunks: aggregateChatCompletionChunks, + }); + + // Beta Chat Completions Stream (sync method returning event-based stream) + traceSyncStreamChannel(openAIChannels.betaChatCompletionsStream, { + name: "Chat Completion", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { messages, ...metadata } = params; + return { + input: processInputAttachments(messages), + metadata: { ...metadata, provider: "openai" }, + }; + }, + }); + + // Moderations + traceAsyncChannel(openAIChannels.moderationsCreate, { + name: "Moderation", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { input, ...metadata } = params; + return { + input, + metadata: { ...metadata, provider: "openai" }, + }; + }, + extractOutput: (result) => { + return result?.results; + }, + extractMetrics: (result, _startTime, endEvent) => { + return withCachedMetric( + parseMetricsFromUsage(result?.usage), + result, + endEvent, + ); + }, + }); + + // Responses API - create (supports streaming via stream=true param) + traceStreamingChannel(openAIChannels.responsesCreate, { + name: "openai.responses.create", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { input, ...metadata } = params; + return { + input: processInputAttachments(input), + metadata: { ...metadata, provider: "openai" }, + }; + }, + extractOutput: (result) => { + return processImagesInOutput(result?.output); + }, + extractMetadata: (result) => { + if (!result) { + return undefined; + } + const { output: _output, usage: _usage, ...metadata } = result; + return Object.keys(metadata).length > 0 ? metadata : undefined; + }, + extractMetrics: (result, startTime, endEvent) => { + const metrics = withCachedMetric( + parseMetricsFromUsage(result?.usage), + result, + endEvent, + ); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + aggregateChunks: aggregateResponseStreamEvents, + }); + + // Responses API - stream (sync method returning event-based stream) + traceSyncStreamChannel(openAIChannels.responsesStream, { + name: "openai.responses.create", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { input, ...metadata } = params; + return { + input: processInputAttachments(input), + metadata: { ...metadata, provider: "openai" }, + }; + }, + extractFromEvent: (event) => { + if (event.type !== "response.completed" || !event.response) { + return {}; + } + + const response = event.response; + const data: Record = {}; + + if (response.output !== undefined) { + data.output = processImagesInOutput(response.output); + } + + const { usage: _usage, output: _output, ...metadata } = response; + if (Object.keys(metadata).length > 0) { + data.metadata = metadata; + } + + data.metrics = parseMetricsFromUsage(response.usage); + return data; + }, + }); + + // Responses API - parse + traceStreamingChannel(openAIChannels.responsesParse, { + name: "openai.responses.parse", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { input, ...metadata } = params; + return { + input: processInputAttachments(input), + metadata: { ...metadata, provider: "openai" }, + }; + }, + extractOutput: (result) => { + return processImagesInOutput(result?.output); + }, + extractMetadata: (result) => { + if (!result) { + return undefined; + } + const { output: _output, usage: _usage, ...metadata } = result; + return Object.keys(metadata).length > 0 ? metadata : undefined; + }, + extractMetrics: (result, startTime, endEvent) => { + const metrics = withCachedMetric( + parseMetricsFromUsage(result?.usage), + result, + endEvent, + ); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + aggregateChunks: aggregateResponseStreamEvents, + }); + + // Responses API - compact + traceAsyncChannel(openAIChannels.responsesCompact, { + name: "openai.responses.compact", + type: SpanTypeAttribute.LLM, + extractInput: ([params]) => { + const { input, ...metadata } = params; + return { + input: processInputAttachments(input), + metadata: { ...metadata, provider: "openai" }, + }; + }, + extractOutput: (result) => { + return processImagesInOutput(result?.output); + }, + extractMetadata: (result) => { + if (!result) { + return undefined; + } + const { output: _output, usage: _usage, ...metadata } = result; + return Object.keys(metadata).length > 0 ? metadata : undefined; + }, + extractMetrics: (result, startTime, endEvent) => { + const metrics = withCachedMetric( + parseMetricsFromUsage(result?.usage), + result, + endEvent, + ); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + }); +} + +function getCachedMetricFromEndEvent(endEvent: unknown): number | undefined { + if (!isObject(endEvent)) { + return undefined; + } + + const response = (endEvent as Record).response; + if (!isObject(response)) { + return undefined; + } + + const headers = (response as { headers?: unknown }).headers; + if (!headers || typeof (headers as Headers).get !== "function") { + return undefined; + } + + return getCachedMetricFromHeaders(headers as Headers); +} + +function withCachedMetric( + metrics: Record, + result: unknown, + endEvent?: unknown, +): Record { + if (metrics.cached !== undefined) { + return metrics; + } + + const cachedFromEvent = getCachedMetricFromEndEvent(endEvent); + if (cachedFromEvent !== undefined) { + return { + ...metrics, + cached: cachedFromEvent, + }; + } + + if (!isObject(result)) { + return metrics; + } + + const cached = (result as Record)[ + BRAINTRUST_CACHED_STREAM_METRIC + ]; + + if (typeof cached !== "number") { + return metrics; + } + + return { + ...metrics, + cached, + }; +} + +/** + * Process output to convert base64 images to attachments. + * Used for Responses API image generation output. + */ +export function processImagesInOutput(output: any): any { + if (Array.isArray(output)) { + return output.map(processImagesInOutput); + } + + if (isObject(output)) { + if ( + output.type === "image_generation_call" && + output.result && + typeof output.result === "string" + ) { + const fileExtension = output.output_format || "png"; + const contentType = `image/${fileExtension}`; + + const baseFilename = + output.revised_prompt && typeof output.revised_prompt === "string" + ? output.revised_prompt.slice(0, 50).replace(/[^a-zA-Z0-9]/g, "_") + : "generated_image"; + const filename = `${baseFilename}.${fileExtension}`; + + // Convert base64 string to Blob + const binaryString = atob(output.result); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const blob = new Blob([bytes], { type: contentType }); + + const attachment = new Attachment({ + data: blob, + filename: filename, + contentType: contentType, + }); + + return { + ...output, + result: attachment, + }; + } + } + + return output; +} + +function mergeLogprobTokens( + existing: OpenAIChatLogprobs["content"] | OpenAIChatLogprobs["refusal"], + incoming: OpenAIChatLogprobs["content"] | OpenAIChatLogprobs["refusal"], +): OpenAIChatLogprobs["content"] | OpenAIChatLogprobs["refusal"] { + if (incoming === undefined) { + return existing; + } + + if (incoming === null) { + return existing ?? null; + } + + if (Array.isArray(existing)) { + return [...existing, ...incoming]; + } + + return [...incoming]; +} + +function aggregateChatLogprobs( + existing: OpenAIChatLogprobs | null | undefined, + incoming: OpenAIChatLogprobs | null | undefined, +): OpenAIChatLogprobs | null | undefined { + if (incoming === undefined) { + return existing; + } + + if (incoming === null) { + return existing ?? null; + } + + const aggregated: OpenAIChatLogprobs = + existing && existing !== null + ? { ...existing, ...incoming } + : { ...incoming }; + + const content = mergeLogprobTokens(existing?.content, incoming.content); + if (content !== undefined) { + aggregated.content = content; + } + + const refusal = mergeLogprobTokens(existing?.refusal, incoming.refusal); + if (refusal !== undefined) { + aggregated.refusal = refusal; + } + + return aggregated; +} + +/** + * Aggregate chat completion chunks into a single response. + * Combines role (first), content (concatenated), tool_calls (by id), + * finish_reason (last), and usage (last chunk). + */ +export function aggregateChatCompletionChunks( + chunks: OpenAIChatCompletionChunk[], + streamResult?: unknown, + endEvent?: unknown, +): { + output: OpenAIChatChoice[]; + metrics: Record; +} { + let role = undefined; + let content = undefined; + let refusal = undefined; + let tool_calls = undefined; + let logprobs: OpenAIChatLogprobs | null | undefined = undefined; + let finish_reason = undefined; + let metrics: Record = {}; + + for (const chunk of chunks) { + if (chunk.usage) { + metrics = { + ...metrics, + ...parseMetricsFromUsage(chunk.usage), + }; + } + + const choice = chunk.choices?.[0]; + if (!choice) { + continue; + } + + if (choice.finish_reason) { + finish_reason = choice.finish_reason; + } + + logprobs = aggregateChatLogprobs(logprobs, choice.logprobs); + + const delta = choice.delta; + if (!delta) { + continue; + } + + if (delta.finish_reason) { + finish_reason = delta.finish_reason; + } + + if (!role && delta.role) { + role = delta.role; + } + + if (delta.content) { + content = (content || "") + delta.content; + } + + if (delta.refusal) { + refusal = (refusal || "") + delta.refusal; + } + + if (delta.tool_calls) { + const toolDelta = delta.tool_calls[0]; + if ( + !tool_calls || + (toolDelta.id && tool_calls[tool_calls.length - 1].id !== toolDelta.id) + ) { + tool_calls = [ + ...(tool_calls || []), + { + id: toolDelta.id, + type: toolDelta.type, + function: toolDelta.function, + }, + ]; + } else { + tool_calls[tool_calls.length - 1].function.arguments += + toolDelta.function.arguments; + } + } + } + + metrics = withCachedMetric(metrics, streamResult, endEvent); + + return { + metrics, + output: [ + { + index: 0, + message: { + role, + content, + ...(refusal !== undefined ? { refusal } : {}), + tool_calls, + }, + logprobs: logprobs ?? null, + finish_reason, + }, + ], + }; +} + +function aggregateResponseStreamEvents( + chunks: OpenAIResponseStreamEvent[], + _streamResult?: unknown, + endEvent?: unknown, +): { + output: any; + metrics: Record; + metadata?: Record; +} { + let output: any = undefined; + let metrics: Record = {}; + let metadata: Record | undefined = undefined; + + for (const chunk of chunks) { + if (!chunk || !chunk.type || !chunk.response) { + continue; + } + if (chunk.type !== "response.completed") { + continue; + } + + const response = chunk.response; + if (response?.output !== undefined) { + output = processImagesInOutput(response.output); + } + + const { usage: _usage, output: _output, ...rest } = response || {}; + if (Object.keys(rest).length > 0) { + metadata = rest; + } + + metrics = parseMetricsFromUsage(response?.usage); + } + + return { + output, + metrics: withCachedMetric(metrics, undefined, endEvent), + ...(metadata !== undefined ? { metadata } : {}), + }; +} + +export { parseMetricsFromUsage }; diff --git a/js/src/instrumentation/plugins/openrouter-agent-channels.ts b/js/src/instrumentation/providers/openrouter-agent-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/openrouter-agent-channels.ts rename to js/src/instrumentation/providers/openrouter-agent-channels.ts diff --git a/js/src/instrumentation/plugins/openrouter-agent-plugin.test.ts b/js/src/instrumentation/providers/openrouter-agent-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/openrouter-agent-plugin.test.ts rename to js/src/instrumentation/providers/openrouter-agent-instrumentation.test.ts index 85d66d51c..27fa78ca2 100644 --- a/js/src/instrumentation/plugins/openrouter-agent-plugin.test.ts +++ b/js/src/instrumentation/providers/openrouter-agent-instrumentation.test.ts @@ -14,7 +14,7 @@ import { aggregateOpenRouterChatChunks, aggregateOpenRouterResponseStreamEvents, parseOpenRouterMetricsFromUsage, -} from "./openrouter-agent-plugin"; +} from "./openrouter-agent-instrumentation"; const TEST_PROVIDER = "openai"; const TEST_MODEL = "gpt-4.1-mini"; @@ -25,7 +25,7 @@ try { // Best-effort initialization for test environments. } -describe("OpenRouter Agent Plugin", () => { +describe("OpenRouter Agent instrumentation", () => { let backgroundLogger: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; @@ -37,7 +37,7 @@ describe("OpenRouter Agent Plugin", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "openrouter-agent-plugin.test.ts", + projectName: "openrouter-agent-instrumentation.test.ts", projectId: "test-project-id", }); }); diff --git a/js/src/instrumentation/plugins/openrouter-agent-plugin.ts b/js/src/instrumentation/providers/openrouter-agent-instrumentation.ts similarity index 91% rename from js/src/instrumentation/plugins/openrouter-agent-plugin.ts rename to js/src/instrumentation/providers/openrouter-agent-instrumentation.ts index facda63c0..7f77cccfc 100644 --- a/js/src/instrumentation/plugins/openrouter-agent-plugin.ts +++ b/js/src/instrumentation/providers/openrouter-agent-instrumentation.ts @@ -1,9 +1,8 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import { traceAsyncChannel, traceStreamingChannel, traceSyncStreamChannel, - unsubscribeAll, } from "../core/channel-tracing"; import type { ChannelMessage } from "../core/channel-definitions"; import { @@ -26,107 +25,93 @@ import type { OpenRouterAgentToolTurnContext, } from "../../vendor-sdk-types/openrouter-agent"; -export class OpenRouterAgentPlugin extends BasePlugin { - protected onEnable(): void { +class OpenRouterAgentInstrumentationConsumer { + public register(): void { this.subscribeToOpenRouterAgentChannels(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } - private subscribeToOpenRouterAgentChannels(): void { - this.unsubscribers.push( - traceSyncStreamChannel(openRouterAgentChannels.callModel, { - name: "openrouter.callModel", - type: SpanTypeAttribute.TASK, - extractInput: (args) => { - const request = getOpenRouterCallModelRequestArg(args); - return { - input: request - ? extractOpenRouterCallModelInput(request) - : undefined, - metadata: request - ? extractOpenRouterCallModelMetadata(request) - : { provider: "openrouter" }, - }; - }, - patchResult: ({ endEvent, result, span }) => { - return patchOpenRouterCallModelResult({ - request: getOpenRouterCallModelRequestArg(endEvent.arguments), - result, - span, - }); - }, - }), - ); - - this.unsubscribers.push( - traceAsyncChannel(openRouterAgentChannels.callModelTurn, { - name: "openrouter.beta.responses.send", - type: SpanTypeAttribute.LLM, - extractInput: (args, event) => { - const request = getOpenRouterCallModelRequestArg(args); - const metadata = request + traceSyncStreamChannel(openRouterAgentChannels.callModel, { + name: "openrouter.callModel", + type: SpanTypeAttribute.TASK, + extractInput: (args) => { + const request = getOpenRouterCallModelRequestArg(args); + return { + input: request ? extractOpenRouterCallModelInput(request) : undefined, + metadata: request ? extractOpenRouterCallModelMetadata(request) - : { provider: "openrouter" }; + : { provider: "openrouter" }, + }; + }, + patchResult: ({ endEvent, result, span }) => { + return patchOpenRouterCallModelResult({ + request: getOpenRouterCallModelRequestArg(endEvent.arguments), + result, + span, + }); + }, + }); - if (isObject(metadata) && "tools" in metadata) { - delete (metadata as Record).tools; - } + traceAsyncChannel(openRouterAgentChannels.callModelTurn, { + name: "openrouter.beta.responses.send", + type: SpanTypeAttribute.LLM, + extractInput: (args, event) => { + const request = getOpenRouterCallModelRequestArg(args); + const metadata = request + ? extractOpenRouterCallModelMetadata(request) + : { provider: "openrouter" }; + + if (isObject(metadata) && "tools" in metadata) { + delete (metadata as Record).tools; + } + return { + input: request ? extractOpenRouterCallModelInput(request) : undefined, + metadata: { + ...metadata, + step: event.step, + step_type: event.stepType, + }, + }; + }, + extractOutput: (result) => + extractOpenRouterResponseOutput(result as Record), + extractMetadata: (result, event) => { + if (!isObject(result)) { return { - input: request - ? extractOpenRouterCallModelInput(request) - : undefined, - metadata: { - ...metadata, - step: event.step, - step_type: event.stepType, - }, + step: event?.step, + step_type: event?.stepType, }; - }, - extractOutput: (result) => - extractOpenRouterResponseOutput(result as Record), - extractMetadata: (result, event) => { - if (!isObject(result)) { - return { - step: event?.step, - step_type: event?.stepType, - }; - } + } - return { - ...(extractOpenRouterResponseMetadata(result) || {}), - ...(event?.step !== undefined ? { step: event.step } : {}), - ...(event?.stepType ? { step_type: event.stepType } : {}), - }; + return { + ...(extractOpenRouterResponseMetadata(result) || {}), + ...(event?.step !== undefined ? { step: event.step } : {}), + ...(event?.stepType ? { step_type: event.stepType } : {}), + }; + }, + extractMetrics: (result) => + isObject(result) ? parseOpenRouterMetricsFromUsage(result.usage) : {}, + }); + + traceStreamingChannel(openRouterAgentChannels.toolExecute, { + name: "openrouter.tool", + type: SpanTypeAttribute.TOOL, + extractInput: (args, event) => ({ + input: args[0], + metadata: { + provider: "openrouter", + tool_name: event.toolName, + ...(event.toolCallId ? { tool_call_id: event.toolCallId } : {}), }, - extractMetrics: (result) => - isObject(result) ? parseOpenRouterMetricsFromUsage(result.usage) : {}, }), - ); - - this.unsubscribers.push( - traceStreamingChannel(openRouterAgentChannels.toolExecute, { - name: "openrouter.tool", - type: SpanTypeAttribute.TOOL, - extractInput: (args, event) => ({ - input: args[0], - metadata: { - provider: "openrouter", - tool_name: event.toolName, - ...(event.toolCallId ? { tool_call_id: event.toolCallId } : {}), - }, - }), - extractOutput: (result) => result, - extractMetrics: () => ({}), - aggregateChunks: (chunks) => ({ - output: chunks.length > 0 ? chunks[chunks.length - 1] : undefined, - metrics: {}, - }), + extractOutput: (result) => result, + extractMetrics: () => ({}), + aggregateChunks: (chunks) => ({ + output: chunks.length > 0 ? chunks[chunks.length - 1] : undefined, + metrics: {}, }), - ); + }); const callModelChannel = openRouterAgentChannels.callModel.tracingChannel(); const callModelHandlers = { @@ -141,9 +126,6 @@ export class OpenRouterAgentPlugin extends BasePlugin { }; callModelChannel.subscribe(callModelHandlers); - this.unsubscribers.push(() => { - callModelChannel.unsubscribe(callModelHandlers); - }); } } @@ -1326,3 +1308,13 @@ function normalizeError(error: unknown): Error { } export { parseOpenRouterMetricsFromUsage }; + +let openRouterAgentInstrumentationConsumer: + | OpenRouterAgentInstrumentationConsumer + | undefined; + +export function registerOpenRouterAgentInstrumentation(): void { + openRouterAgentInstrumentationConsumer ??= + new OpenRouterAgentInstrumentationConsumer(); + openRouterAgentInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/openrouter-channels.ts b/js/src/instrumentation/providers/openrouter-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/openrouter-channels.ts rename to js/src/instrumentation/providers/openrouter-channels.ts diff --git a/js/src/instrumentation/plugins/openrouter-plugin.test.ts b/js/src/instrumentation/providers/openrouter-instrumentation.test.ts similarity index 99% rename from js/src/instrumentation/plugins/openrouter-plugin.test.ts rename to js/src/instrumentation/providers/openrouter-instrumentation.test.ts index 93627512e..c656d5e6d 100644 --- a/js/src/instrumentation/plugins/openrouter-plugin.test.ts +++ b/js/src/instrumentation/providers/openrouter-instrumentation.test.ts @@ -14,7 +14,7 @@ import { aggregateOpenRouterChatChunks, aggregateOpenRouterResponseStreamEvents, parseOpenRouterMetricsFromUsage, -} from "./openrouter-plugin"; +} from "./openrouter-instrumentation"; const TEST_PROVIDER = "openai"; const TEST_MODEL = "gpt-4.1-mini"; @@ -27,7 +27,7 @@ try { // Best-effort initialization for test environments. } -describe("OpenRouter Plugin", () => { +describe("OpenRouter instrumentation", () => { let backgroundLogger: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; @@ -39,7 +39,7 @@ describe("OpenRouter Plugin", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "openrouter-plugin.test.ts", + projectName: "openrouter-instrumentation.test.ts", projectId: "test-project-id", }); }); diff --git a/js/src/instrumentation/plugins/openrouter-plugin.ts b/js/src/instrumentation/providers/openrouter-instrumentation.ts similarity index 80% rename from js/src/instrumentation/plugins/openrouter-plugin.ts rename to js/src/instrumentation/providers/openrouter-instrumentation.ts index c89b8f9a7..fa8e54aaa 100644 --- a/js/src/instrumentation/plugins/openrouter-plugin.ts +++ b/js/src/instrumentation/providers/openrouter-instrumentation.ts @@ -1,9 +1,8 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import { traceAsyncChannel, traceStreamingChannel, traceSyncStreamChannel, - unsubscribeAll, } from "../core/channel-tracing"; import type { ChannelMessage } from "../core/channel-definitions"; import { @@ -28,247 +27,225 @@ import type { OpenRouterToolTurnContext, } from "../../vendor-sdk-types/openrouter"; -export class OpenRouterPlugin extends BasePlugin { - protected onEnable(): void { +class OpenRouterInstrumentationConsumer { + public register(): void { this.subscribeToOpenRouterChannels(); } - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } - private subscribeToOpenRouterChannels(): void { - this.unsubscribers.push( - traceStreamingChannel(openRouterChannels.chatSend, { - name: "openrouter.chat.send", - type: SpanTypeAttribute.LLM, - extractInput: (args) => { - const request = getOpenRouterRequestArg(args); - const chatGenerationParams = isObject(request?.chatGenerationParams) - ? request.chatGenerationParams - : {}; - const httpReferer = request?.httpReferer; - const xTitle = request?.xTitle; - const { messages, ...metadata } = chatGenerationParams; - return { - input: messages, - metadata: buildOpenRouterMetadata(metadata, httpReferer, xTitle), - }; - }, - extractOutput: (result) => { - return isObject(result) ? result.choices : undefined; - }, - extractMetrics: (result, startTime) => { - const metrics = parseOpenRouterMetricsFromUsage(result?.usage); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - aggregateChunks: aggregateOpenRouterChatChunks, - }), - ); - - this.unsubscribers.push( - traceAsyncChannel(openRouterChannels.embeddingsGenerate, { - name: "openrouter.embeddings.generate", - type: SpanTypeAttribute.LLM, - extractInput: (args) => { - const request = getOpenRouterRequestArg(args); - const requestBody = isObject(request?.requestBody) - ? request.requestBody - : {}; - const httpReferer = request?.httpReferer; - const xTitle = request?.xTitle; - const { input, ...metadata } = requestBody; - return { - input, - metadata: buildOpenRouterEmbeddingMetadata( - metadata, - httpReferer, - xTitle, - ), - }; - }, - extractOutput: (result) => { - if (!isObject(result)) { - return undefined; - } + traceStreamingChannel(openRouterChannels.chatSend, { + name: "openrouter.chat.send", + type: SpanTypeAttribute.LLM, + extractInput: (args) => { + const request = getOpenRouterRequestArg(args); + const chatGenerationParams = isObject(request?.chatGenerationParams) + ? request.chatGenerationParams + : {}; + const httpReferer = request?.httpReferer; + const xTitle = request?.xTitle; + const { messages, ...metadata } = chatGenerationParams; + return { + input: messages, + metadata: buildOpenRouterMetadata(metadata, httpReferer, xTitle), + }; + }, + extractOutput: (result) => { + return isObject(result) ? result.choices : undefined; + }, + extractMetrics: (result, startTime) => { + const metrics = parseOpenRouterMetricsFromUsage(result?.usage); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + aggregateChunks: aggregateOpenRouterChatChunks, + }); - const embedding = result.data?.[0]?.embedding; - return Array.isArray(embedding) - ? { embedding_length: embedding.length } - : undefined; - }, - extractMetadata: (result) => { - if (!isObject(result)) { - return undefined; - } + traceAsyncChannel(openRouterChannels.embeddingsGenerate, { + name: "openrouter.embeddings.generate", + type: SpanTypeAttribute.LLM, + extractInput: (args) => { + const request = getOpenRouterRequestArg(args); + const requestBody = isObject(request?.requestBody) + ? request.requestBody + : {}; + const httpReferer = request?.httpReferer; + const xTitle = request?.xTitle; + const { input, ...metadata } = requestBody; + return { + input, + metadata: buildOpenRouterEmbeddingMetadata( + metadata, + httpReferer, + xTitle, + ), + }; + }, + extractOutput: (result) => { + if (!isObject(result)) { + return undefined; + } - return extractOpenRouterResponseMetadata(result); - }, - extractMetrics: (result) => { - return isObject(result) - ? parseOpenRouterMetricsFromUsage(result.usage) - : {}; - }, - }), - ); + const embedding = result.data?.[0]?.embedding; + return Array.isArray(embedding) + ? { embedding_length: embedding.length } + : undefined; + }, + extractMetadata: (result) => { + if (!isObject(result)) { + return undefined; + } - this.unsubscribers.push( - traceAsyncChannel(openRouterChannels.rerankRerank, { - name: "openrouter.rerank.rerank", - type: SpanTypeAttribute.LLM, - extractInput: (args) => { - const request = getOpenRouterRequestArg(args); - const requestBody = isObject(request?.requestBody) - ? request.requestBody - : {}; - const httpReferer = request?.httpReferer; - const xTitle = request?.xTitle ?? request?.appTitle; - const { documents, query, ...metadata } = requestBody; - return { - input: { - documents, - query, - }, - metadata: buildOpenRouterRerankMetadata( - metadata, - documents, - httpReferer, - xTitle, - ), - }; - }, - extractOutput: (result) => extractOpenRouterRerankOutput(result), - extractMetadata: (result) => extractOpenRouterResponseMetadata(result), - extractMetrics: (result) => - isObject(result) ? parseOpenRouterMetricsFromUsage(result.usage) : {}, - }), - ); + return extractOpenRouterResponseMetadata(result); + }, + extractMetrics: (result) => { + return isObject(result) + ? parseOpenRouterMetricsFromUsage(result.usage) + : {}; + }, + }); - this.unsubscribers.push( - traceStreamingChannel(openRouterChannels.betaResponsesSend, { - name: "openrouter.beta.responses.send", - type: SpanTypeAttribute.LLM, - extractInput: (args) => { - const request = getOpenRouterRequestArg(args); - const openResponsesRequest = isObject(request?.openResponsesRequest) - ? request.openResponsesRequest - : {}; - const httpReferer = request?.httpReferer; - const xTitle = request?.xTitle; - const { input, ...metadata } = openResponsesRequest; - return { - input, - metadata: buildOpenRouterMetadata(metadata, httpReferer, xTitle), - }; - }, - extractOutput: (result) => - extractOpenRouterResponseOutput(result as Record), - extractMetadata: (result) => extractOpenRouterResponseMetadata(result), - extractMetrics: (result, startTime) => { - const metrics = parseOpenRouterMetricsFromUsage(result?.usage); - if (startTime) { - metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; - } - return metrics; - }, - aggregateChunks: aggregateOpenRouterResponseStreamEvents, - }), - ); + traceAsyncChannel(openRouterChannels.rerankRerank, { + name: "openrouter.rerank.rerank", + type: SpanTypeAttribute.LLM, + extractInput: (args) => { + const request = getOpenRouterRequestArg(args); + const requestBody = isObject(request?.requestBody) + ? request.requestBody + : {}; + const httpReferer = request?.httpReferer; + const xTitle = request?.xTitle ?? request?.appTitle; + const { documents, query, ...metadata } = requestBody; + return { + input: { + documents, + query, + }, + metadata: buildOpenRouterRerankMetadata( + metadata, + documents, + httpReferer, + xTitle, + ), + }; + }, + extractOutput: (result) => extractOpenRouterRerankOutput(result), + extractMetadata: (result) => extractOpenRouterResponseMetadata(result), + extractMetrics: (result) => + isObject(result) ? parseOpenRouterMetricsFromUsage(result.usage) : {}, + }); - this.unsubscribers.push( - traceSyncStreamChannel(openRouterChannels.callModel, { - name: "openrouter.callModel", - type: SpanTypeAttribute.TASK, - extractInput: (args) => { - const request = getOpenRouterCallModelRequestArg(args); - return { - input: request - ? extractOpenRouterCallModelInput(request) - : undefined, - metadata: request - ? extractOpenRouterCallModelMetadata(request) - : { provider: "openrouter" }, - }; - }, - patchResult: ({ endEvent, result, span }) => { - return patchOpenRouterCallModelResult({ - request: getOpenRouterCallModelRequestArg(endEvent.arguments), - result, - span, - }); - }, - }), - ); + traceStreamingChannel(openRouterChannels.betaResponsesSend, { + name: "openrouter.beta.responses.send", + type: SpanTypeAttribute.LLM, + extractInput: (args) => { + const request = getOpenRouterRequestArg(args); + const openResponsesRequest = isObject(request?.openResponsesRequest) + ? request.openResponsesRequest + : {}; + const httpReferer = request?.httpReferer; + const xTitle = request?.xTitle; + const { input, ...metadata } = openResponsesRequest; + return { + input, + metadata: buildOpenRouterMetadata(metadata, httpReferer, xTitle), + }; + }, + extractOutput: (result) => + extractOpenRouterResponseOutput(result as Record), + extractMetadata: (result) => extractOpenRouterResponseMetadata(result), + extractMetrics: (result, startTime) => { + const metrics = parseOpenRouterMetricsFromUsage(result?.usage); + if (startTime) { + metrics.time_to_first_token = getCurrentUnixTimestamp() - startTime; + } + return metrics; + }, + aggregateChunks: aggregateOpenRouterResponseStreamEvents, + }); - this.unsubscribers.push( - traceAsyncChannel(openRouterChannels.callModelTurn, { - name: "openrouter.beta.responses.send", - type: SpanTypeAttribute.LLM, - extractInput: (args, event) => { - const request = getOpenRouterCallModelRequestArg(args); - const metadata = request + traceSyncStreamChannel(openRouterChannels.callModel, { + name: "openrouter.callModel", + type: SpanTypeAttribute.TASK, + extractInput: (args) => { + const request = getOpenRouterCallModelRequestArg(args); + return { + input: request ? extractOpenRouterCallModelInput(request) : undefined, + metadata: request ? extractOpenRouterCallModelMetadata(request) - : { provider: "openrouter" }; + : { provider: "openrouter" }, + }; + }, + patchResult: ({ endEvent, result, span }) => { + return patchOpenRouterCallModelResult({ + request: getOpenRouterCallModelRequestArg(endEvent.arguments), + result, + span, + }); + }, + }); - if (isObject(metadata) && "tools" in metadata) { - delete (metadata as Record).tools; - } + traceAsyncChannel(openRouterChannels.callModelTurn, { + name: "openrouter.beta.responses.send", + type: SpanTypeAttribute.LLM, + extractInput: (args, event) => { + const request = getOpenRouterCallModelRequestArg(args); + const metadata = request + ? extractOpenRouterCallModelMetadata(request) + : { provider: "openrouter" }; + + if (isObject(metadata) && "tools" in metadata) { + delete (metadata as Record).tools; + } + return { + input: request ? extractOpenRouterCallModelInput(request) : undefined, + metadata: { + ...metadata, + step: event.step, + step_type: event.stepType, + }, + }; + }, + extractOutput: (result) => + extractOpenRouterResponseOutput(result as Record), + extractMetadata: (result, event) => { + if (!isObject(result)) { return { - input: request - ? extractOpenRouterCallModelInput(request) - : undefined, - metadata: { - ...metadata, - step: event.step, - step_type: event.stepType, - }, + step: event?.step, + step_type: event?.stepType, }; - }, - extractOutput: (result) => - extractOpenRouterResponseOutput(result as Record), - extractMetadata: (result, event) => { - if (!isObject(result)) { - return { - step: event?.step, - step_type: event?.stepType, - }; - } + } - return { - ...(extractOpenRouterResponseMetadata(result) || {}), - ...(event?.step !== undefined ? { step: event.step } : {}), - ...(event?.stepType ? { step_type: event.stepType } : {}), - }; + return { + ...(extractOpenRouterResponseMetadata(result) || {}), + ...(event?.step !== undefined ? { step: event.step } : {}), + ...(event?.stepType ? { step_type: event.stepType } : {}), + }; + }, + extractMetrics: (result) => + isObject(result) ? parseOpenRouterMetricsFromUsage(result.usage) : {}, + }); + + traceStreamingChannel(openRouterChannels.toolExecute, { + name: "openrouter.tool", + type: SpanTypeAttribute.TOOL, + extractInput: (args, event) => ({ + input: args[0], + metadata: { + provider: "openrouter", + tool_name: event.toolName, + ...(event.toolCallId ? { tool_call_id: event.toolCallId } : {}), }, - extractMetrics: (result) => - isObject(result) ? parseOpenRouterMetricsFromUsage(result.usage) : {}, }), - ); - - this.unsubscribers.push( - traceStreamingChannel(openRouterChannels.toolExecute, { - name: "openrouter.tool", - type: SpanTypeAttribute.TOOL, - extractInput: (args, event) => ({ - input: args[0], - metadata: { - provider: "openrouter", - tool_name: event.toolName, - ...(event.toolCallId ? { tool_call_id: event.toolCallId } : {}), - }, - }), - extractOutput: (result) => result, - extractMetrics: () => ({}), - aggregateChunks: (chunks) => ({ - output: chunks.length > 0 ? chunks[chunks.length - 1] : undefined, - metrics: {}, - }), + extractOutput: (result) => result, + extractMetrics: () => ({}), + aggregateChunks: (chunks) => ({ + output: chunks.length > 0 ? chunks[chunks.length - 1] : undefined, + metrics: {}, }), - ); + }); const callModelChannel = openRouterChannels.callModel.tracingChannel(); const callModelHandlers = { @@ -283,9 +260,6 @@ export class OpenRouterPlugin extends BasePlugin { }; callModelChannel.subscribe(callModelHandlers); - this.unsubscribers.push(() => { - callModelChannel.unsubscribe(callModelHandlers); - }); } } @@ -1508,3 +1482,12 @@ function normalizeError(error: unknown): Error { } export { parseOpenRouterMetricsFromUsage }; + +let openRouterInstrumentationConsumer: + | OpenRouterInstrumentationConsumer + | undefined; + +export function registerOpenRouterInstrumentation(): void { + openRouterInstrumentationConsumer ??= new OpenRouterInstrumentationConsumer(); + openRouterInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/pi-coding-agent-channels.ts b/js/src/instrumentation/providers/pi-coding-agent-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/pi-coding-agent-channels.ts rename to js/src/instrumentation/providers/pi-coding-agent-channels.ts diff --git a/js/src/instrumentation/plugins/pi-coding-agent-plugin.test.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts similarity index 91% rename from js/src/instrumentation/plugins/pi-coding-agent-plugin.test.ts rename to js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts index 433361974..7e7d406ed 100644 --- a/js/src/instrumentation/plugins/pi-coding-agent-plugin.test.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts @@ -30,7 +30,7 @@ vi.mock("../../logger", () => ({ })); import { isAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; -import { PiCodingAgentPlugin } from "./pi-coding-agent-plugin"; +import { registerPiCodingAgentInstrumentation } from "./pi-coding-agent-instrumentation"; const PROMPT_CHANNEL = "orchestrion:@earendil-works/pi-coding-agent:AgentSession.prompt"; @@ -43,12 +43,10 @@ type TestSpan = { name?: string; }; -describe("PiCodingAgentPlugin", () => { - let plugins: PiCodingAgentPlugin[]; +describe("registerPiCodingAgentInstrumentation", () => { let spans: TestSpan[]; beforeEach(() => { - plugins = []; spans = []; interceptorsByName.clear(); mockNewTracingChannel.mockImplementation((name: string) => ({ @@ -74,25 +72,19 @@ describe("PiCodingAgentPlugin", () => { }); afterEach(() => { - for (const plugin of plugins) { - plugin.disable(); - } vi.clearAllMocks(); }); - it("registers and removes the prompt interceptor", () => { - const plugin = enablePlugin(plugins); + it("registers the prompt interceptor for the process lifetime", () => { + registerPiCodingAgentInstrumentation(); expect(interceptorsByName.has(PROMPT_CHANNEL)).toBe(true); expect(mockRemoveInterceptor).not.toHaveBeenCalled(); - - plugin.disable(); - - expect(mockRemoveInterceptor).toHaveBeenCalledTimes(1); }); it("uses interceptor ALS for prompt, LLM, and tool spans", async () => { - const interceptor = promptInterceptor(enablePlugin(plugins)); + registerPiCodingAgentInstrumentation(); + const interceptor = promptInterceptor(); const finalMessage = makeAssistantMessage("done"); const originalStreamFn = vi.fn(async () => { expect(isAutoInstrumentationSuppressed()).toBe(true); @@ -200,7 +192,8 @@ describe("PiCodingAgentPlugin", () => { }); it("isolates overlapping prompts on the same agent without prompt matching", async () => { - const interceptor = promptInterceptor(enablePlugin(plugins)); + registerPiCodingAgentInstrumentation(); + const interceptor = promptInterceptor(); const originalStreamFn = vi.fn(async () => makeStream(makeAssistantMessage("done")), ); @@ -258,7 +251,8 @@ describe("PiCodingAgentPlugin", () => { }); it("keeps deferred follow-up prompts open for their ALS-owned turn", async () => { - const interceptor = promptInterceptor(enablePlugin(plugins)); + registerPiCodingAgentInstrumentation(); + const interceptor = promptInterceptor(); const agent = makeAgent( vi.fn(async () => makeStream(makeAssistantMessage("done"))), ); @@ -296,7 +290,8 @@ describe("PiCodingAgentPlugin", () => { }); it("preserves full-iterator stream behavior", async () => { - const interceptor = promptInterceptor(enablePlugin(plugins)); + registerPiCodingAgentInstrumentation(); + const interceptor = promptInterceptor(); const message = makeAssistantMessage("done"); const { result, stream } = makeIteratorBackedStream([ { partial: message, type: "start" }, @@ -331,7 +326,8 @@ describe("PiCodingAgentPlugin", () => { }); it("forwards iterator failures and closes the underlying iterator", async () => { - const interceptor = promptInterceptor(enablePlugin(plugins)); + registerPiCodingAgentInstrumentation(); + const interceptor = promptInterceptor(); const { iterator, stream } = makeIteratorBackedStream([]); iterator.next.mockRejectedValueOnce(new Error("stream next failed")); const agent = makeAgent(vi.fn(async () => stream)); @@ -364,7 +360,8 @@ describe("PiCodingAgentPlugin", () => { }); it("closes active prompt and LLM spans when the target rejects", async () => { - const interceptor = promptInterceptor(enablePlugin(plugins)); + registerPiCodingAgentInstrumentation(); + const interceptor = promptInterceptor(); const agent = makeAgent( vi.fn(async () => makeStream(makeAssistantMessage("unused"))), ); @@ -396,9 +393,9 @@ describe("PiCodingAgentPlugin", () => { expect(taskSpan?.end).toHaveBeenCalledTimes(1); }); - it("ends in-flight prompts on disable while leaving the safe patch installed", async () => { - const plugin = enablePlugin(plugins); - const interceptor = promptInterceptor(plugin); + it("keeps in-flight prompts active until the call completes", async () => { + registerPiCodingAgentInstrumentation(); + const interceptor = promptInterceptor(); const originalStreamFn = vi.fn(async () => makeStream(makeAssistantMessage("done")), ); @@ -416,12 +413,10 @@ describe("PiCodingAgentPlugin", () => { ); const patchedStreamFn = agent.streamFn; - plugin.disable(); - const taskSpan = findSpan(spans, "AgentSession.prompt"); expect(agent.streamFn).toBe(patchedStreamFn); expect(agent.streamFn).not.toBe(originalStreamFn); - expect(taskSpan?.end).toHaveBeenCalledTimes(1); + expect(taskSpan?.end).not.toHaveBeenCalled(); targetGate.resolve(); await result; @@ -429,14 +424,7 @@ describe("PiCodingAgentPlugin", () => { }); }); -function enablePlugin(plugins: PiCodingAgentPlugin[]): PiCodingAgentPlugin { - const plugin = new PiCodingAgentPlugin(); - plugins.push(plugin); - plugin.enable(); - return plugin; -} - -function promptInterceptor(_plugin: PiCodingAgentPlugin): any { +function promptInterceptor(): any { const interceptor = interceptorsByName.get(PROMPT_CHANNEL); expect(interceptor).toEqual(expect.any(Function)); return interceptor; diff --git a/js/src/instrumentation/plugins/pi-coding-agent-plugin.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts similarity index 93% rename from js/src/instrumentation/plugins/pi-coding-agent-plugin.ts rename to js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts index da6c50b30..11829e004 100644 --- a/js/src/instrumentation/plugins/pi-coding-agent-plugin.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts @@ -1,4 +1,4 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import iso, { type IsoAsyncLocalStorage } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; @@ -44,7 +44,6 @@ type PiPromptState = { deferCompletionUntilTurnEnd: boolean; finalized: boolean; metrics: Record; - onFinalize?: (state: PiPromptState) => void; metadata: Record; output?: unknown; promptCallEnded: boolean; @@ -77,74 +76,52 @@ let piPromptContextStore: | IsoAsyncLocalStorage | undefined; -export class PiCodingAgentPlugin extends BasePlugin { - private readonly activePromptStates = new Set(); - - protected onEnable(): void { +class PiCodingAgentInstrumentationConsumer { + public register(): void { this.interceptPrompt(); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - - for (const state of [...this.activePromptStates]) { - finishPiPromptRun(state); - } - } - private interceptPrompt(): void { - this.unsubscribers.push( - piCodingAgentChannels.prompt.intercept( - (target, thisArg, args, additional) => { - const invokeTarget = () => Reflect.apply(target, thisArg, args); - let state: PiPromptState | undefined; - try { - state = startPiPromptRun( - { - ...additional, - arguments: args, - self: thisArg, - }, - (finalizedState) => { - this.activePromptStates.delete(finalizedState); - }, - ); - } catch (error) { - logInstrumentationError("Pi Coding Agent prompt start", error); - } + void piCodingAgentChannels.prompt.intercept( + (target, thisArg, args, additional) => { + const invokeTarget = () => Reflect.apply(target, thisArg, args); + let state: PiPromptState | undefined; + try { + state = startPiPromptRun({ + ...additional, + arguments: args, + self: thisArg, + }); + } catch (error) { + logInstrumentationError("Pi Coding Agent prompt start", error); + } - if (!state) { - return runWithAutoInstrumentationSuppressed(invokeTarget); - } - this.activePromptStates.add(state); + if (!state) { + return runWithAutoInstrumentationSuppressed(invokeTarget); + } + return promptContextStore().run(state, () => + runWithAutoInstrumentationSuppressed(() => { + let result: PromiseLike; + try { + result = invokeTarget(); + } catch (error) { + finishPiPromptRun(state, error); + throw error; + } - return promptContextStore().run(state, () => - runWithAutoInstrumentationSuppressed(() => { - let result: PromiseLike; - try { - result = invokeTarget(); - } catch (error) { + return Promise.resolve(result).then( + (value) => { + finishPiPromptCall(state); + return value; + }, + (error) => { finishPiPromptRun(state, error); throw error; - } - - return Promise.resolve(result).then( - (value) => { - finishPiPromptCall(state); - return value; - }, - (error) => { - finishPiPromptRun(state, error); - throw error; - }, - ); - }), - ); - }, - ), + }, + ); + }), + ); + }, ); } } @@ -159,7 +136,6 @@ function finishPiPromptCall(state: PiPromptState): void { function startPiPromptRun( event: ChannelMessage, - onFinalize?: (state: PiPromptState) => void, ): PiPromptState | undefined { const session = extractSession(event); const agent = session?.agent; @@ -206,7 +182,6 @@ function startPiPromptRun( finalized: false, metadata, metrics: {}, - onFinalize, promptCallEnded: false, span, startTime: getCurrentUnixTimestamp(), @@ -595,7 +570,6 @@ function finishPiPromptRun(state: PiPromptState, error?: unknown): void { return; } state.finalized = true; - state.onFinalize?.(state); finishOpenLlmSpans(state, error); finishOpenToolSpans(state, error); @@ -1085,3 +1059,13 @@ function safeLog(span: Span, event: Parameters[0]): void { function logInstrumentationError(context: string, error: unknown): void { debugLogger.debug(`${context}:`, error); } + +let piCodingAgentInstrumentationConsumer: + | PiCodingAgentInstrumentationConsumer + | undefined; + +export function registerPiCodingAgentInstrumentation(): void { + piCodingAgentInstrumentationConsumer ??= + new PiCodingAgentInstrumentationConsumer(); + piCodingAgentInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/strands-agent-sdk-channels.ts b/js/src/instrumentation/providers/strands-agent-sdk-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/strands-agent-sdk-channels.ts rename to js/src/instrumentation/providers/strands-agent-sdk-channels.ts diff --git a/js/src/instrumentation/plugins/strands-agent-sdk-plugin.test.ts b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts similarity index 95% rename from js/src/instrumentation/plugins/strands-agent-sdk-plugin.test.ts rename to js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts index 54340d1a3..5c0986ca8 100644 --- a/js/src/instrumentation/plugins/strands-agent-sdk-plugin.test.ts +++ b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts @@ -5,7 +5,6 @@ const { mockWithCurrent, mockNewAsyncLocalStorage, mockStartSpan, - mockUnbindStore, } = vi.hoisted(() => ({ mockBindStore: vi.fn(), mockWithCurrent: vi.fn(), @@ -28,7 +27,6 @@ const { }; }), mockStartSpan: vi.fn(), - mockUnbindStore: vi.fn(), })); vi.mock("../../isomorph", () => ({ @@ -51,11 +49,11 @@ vi.mock("../../logger", async (importOriginal) => { import iso from "../../isomorph"; import { Attachment } from "../../logger"; import { isAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; -import { StrandsAgentSDKPlugin } from "./strands-agent-sdk-plugin"; +import { registerStrandsAgentSDKInstrumentation } from "./strands-agent-sdk-instrumentation"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; -describe("StrandsAgentSDKPlugin", () => { +describe("registerStrandsAgentSDKInstrumentation", () => { let handlersByName: Map; let spans: Array<{ args: any; @@ -77,11 +75,9 @@ describe("StrandsAgentSDKPlugin", () => { mockNewTracingChannel.mockImplementation((name: string) => ({ start: { bindStore: mockBindStore, - unbindStore: mockUnbindStore, }, subscribe: vi.fn((handlers) => handlersByName.set(name, handlers)), traceSync: vi.fn((fn) => fn()), - unsubscribe: vi.fn(), })); currentSpan = undefined; mockWithCurrent.mockImplementation((span: any, callback: () => unknown) => { @@ -123,8 +119,7 @@ describe("StrandsAgentSDKPlugin", () => { }); it("subscribes to Strands stream channels and binds suppression", () => { - const plugin = new StrandsAgentSDKPlugin(); - plugin.enable(); + registerStrandsAgentSDKInstrumentation(); expect( handlersByName.has("orchestrion:@strands-agents/sdk:Agent.stream"), @@ -136,15 +131,10 @@ describe("StrandsAgentSDKPlugin", () => { handlersByName.has("orchestrion:@strands-agents/sdk:Swarm.stream"), ).toBe(true); expect(mockBindStore).toHaveBeenCalledTimes(3); - - plugin.disable(); - - expect(mockUnbindStore).toHaveBeenCalledTimes(3); }); it("records agent model and tool spans from stream events", async () => { - const plugin = new StrandsAgentSDKPlugin(); - plugin.enable(); + registerStrandsAgentSDKInstrumentation(); const handlers = handlersByName.get( "orchestrion:@strands-agents/sdk:Agent.stream", @@ -292,8 +282,7 @@ describe("StrandsAgentSDKPlugin", () => { ])( "converts media from %s to one attachment shared by agent and model spans", async (_description, bytes) => { - const plugin = new StrandsAgentSDKPlugin(); - plugin.enable(); + registerStrandsAgentSDKInstrumentation(); const handlers = handlersByName.get( "orchestrion:@strands-agents/sdk:Agent.stream", @@ -353,8 +342,7 @@ describe("StrandsAgentSDKPlugin", () => { ); it("parents nested agent spans under active graph nodes", async () => { - const plugin = new StrandsAgentSDKPlugin(); - plugin.enable(); + registerStrandsAgentSDKInstrumentation(); const graphHandlers = handlersByName.get( "orchestrion:@strands-agents/sdk:Graph.stream", @@ -441,8 +429,7 @@ describe("StrandsAgentSDKPlugin", () => { }); it("parents nested graph spans under active graph nodes", async () => { - const plugin = new StrandsAgentSDKPlugin(); - plugin.enable(); + registerStrandsAgentSDKInstrumentation(); const graphHandlers = handlersByName.get( "orchestrion:@strands-agents/sdk:Graph.stream", @@ -530,8 +517,7 @@ describe("StrandsAgentSDKPlugin", () => { }); it("ends open child spans when a stream errors", async () => { - const plugin = new StrandsAgentSDKPlugin(); - plugin.enable(); + registerStrandsAgentSDKInstrumentation(); const handlers = handlersByName.get( "orchestrion:@strands-agents/sdk:Agent.stream", diff --git a/js/src/instrumentation/plugins/strands-agent-sdk-plugin.ts b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts similarity index 97% rename from js/src/instrumentation/plugins/strands-agent-sdk-plugin.ts rename to js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts index 255578a13..5142f2d5c 100644 --- a/js/src/instrumentation/plugins/strands-agent-sdk-plugin.ts +++ b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts @@ -1,4 +1,4 @@ -import { BasePlugin, toLoggedError } from "../core"; +import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { IsoChannelHandlers } from "../../isomorph"; @@ -101,10 +101,10 @@ type StrandsAttachmentCache = { strings: LRUCache>; }; -export class StrandsAgentSDKPlugin extends BasePlugin { +class StrandsAgentSDKInstrumentationConsumer { private readonly activeChildParents: ActiveChildParents = new WeakMap(); - protected onEnable(): void { + public register(): void { this.subscribeToAgentStream(); this.subscribeToMultiAgentStream( strandsAgentSDKChannels.graphStream, @@ -116,18 +116,10 @@ export class StrandsAgentSDKPlugin extends BasePlugin { ); } - protected onDisable(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers = []; - } - private subscribeToAgentStream(): void { const channel = strandsAgentSDKChannels.agentStream.tracingChannel(); const states = new WeakMap(); - const unbindAutoInstrumentationSuppression = - bindAutoInstrumentationSuppressionToStart(channel); + bindAutoInstrumentationSuppressionToStart(channel); const handlers: IsoChannelHandlers< ChannelMessage @@ -176,10 +168,6 @@ export class StrandsAgentSDKPlugin extends BasePlugin { }; channel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindAutoInstrumentationSuppression?.(); - channel.unsubscribe(handlers); - }); } private subscribeToMultiAgentStream( @@ -188,8 +176,7 @@ export class StrandsAgentSDKPlugin extends BasePlugin { ): void { const tracingChannel = channel.tracingChannel(); const states = new WeakMap(); - const unbindAutoInstrumentationSuppression = - bindAutoInstrumentationSuppressionToStart(tracingChannel); + bindAutoInstrumentationSuppressionToStart(tracingChannel); const handlers: IsoChannelHandlers> = { start: (event) => { @@ -250,10 +237,6 @@ export class StrandsAgentSDKPlugin extends BasePlugin { }; tracingChannel.subscribe(handlers); - this.unsubscribers.push(() => { - unbindAutoInstrumentationSuppression?.(); - tracingChannel.unsubscribe(handlers); - }); } } @@ -1393,3 +1376,13 @@ function safeLog(span: Span, event: Parameters[0]): void { function logInstrumentationError(context: string, error: unknown): void { debugLogger.debug(`${context}:`, error); } + +let strandsAgentSDKInstrumentationConsumer: + | StrandsAgentSDKInstrumentationConsumer + | undefined; + +export function registerStrandsAgentSDKInstrumentation(): void { + strandsAgentSDKInstrumentationConsumer ??= + new StrandsAgentSDKInstrumentationConsumer(); + strandsAgentSDKInstrumentationConsumer.register(); +} diff --git a/js/src/instrumentation/plugins/voyageai-channels.ts b/js/src/instrumentation/providers/voyageai-channels.ts similarity index 100% rename from js/src/instrumentation/plugins/voyageai-channels.ts rename to js/src/instrumentation/providers/voyageai-channels.ts diff --git a/js/src/instrumentation/plugins/voyageai-plugin.test.ts b/js/src/instrumentation/providers/voyageai-instrumentation.test.ts similarity index 98% rename from js/src/instrumentation/plugins/voyageai-plugin.test.ts rename to js/src/instrumentation/providers/voyageai-instrumentation.test.ts index 65cfca93c..2cdb491b4 100644 --- a/js/src/instrumentation/plugins/voyageai-plugin.test.ts +++ b/js/src/instrumentation/providers/voyageai-instrumentation.test.ts @@ -9,7 +9,7 @@ try { // Best-effort initialization for test environments. } -describe("VoyageAIPlugin", () => { +describe("registerVoyageAIInstrumentation", () => { let backgroundLogger: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; @@ -21,7 +21,7 @@ describe("VoyageAIPlugin", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - projectName: "voyageai-plugin.test.ts", + projectName: "voyageai-instrumentation.test.ts", projectId: "test-project-id", }); }); diff --git a/js/src/instrumentation/plugins/voyageai-plugin.ts b/js/src/instrumentation/providers/voyageai-instrumentation.ts similarity index 89% rename from js/src/instrumentation/plugins/voyageai-plugin.ts rename to js/src/instrumentation/providers/voyageai-instrumentation.ts index 37f864b26..ac2304e81 100644 --- a/js/src/instrumentation/plugins/voyageai-plugin.ts +++ b/js/src/instrumentation/providers/voyageai-instrumentation.ts @@ -17,8 +17,6 @@ import { isAutoInstrumentationSuppressed, runWithAutoInstrumentationSuppressed, } from "../auto-instrumentation-suppression"; -import { BasePlugin } from "../core"; -import { unsubscribeAll } from "../core/channel-tracing"; import { voyageAIChannels } from "./voyageai-channels"; const RERANK_METADATA_ALLOWLIST = new Set([ @@ -28,42 +26,34 @@ const RERANK_METADATA_ALLOWLIST = new Set([ "truncation", ]); -export class VoyageAIPlugin extends BasePlugin { - protected onEnable(): void { - this.unsubscribers.push( - interceptVoyageAICall( - voyageAIChannels.embed, - "voyageai.embed", - extractTextEmbeddingInput, - summarizeEmbeddingOutput, - extractEmbeddingUsageMetrics, - ), - interceptVoyageAICall( - voyageAIChannels.multimodalEmbed, - "voyageai.multimodalEmbed", - extractMultimodalEmbeddingInput, - summarizeEmbeddingOutput, - extractEmbeddingUsageMetrics, - ), - interceptVoyageAICall( - voyageAIChannels.rerank, - "voyageai.rerank", - extractRerankInput, - summarizeRerankOutput, - ), - interceptVoyageAICall( - voyageAIChannels.contextualizedEmbed, - "voyageai.contextualizedEmbed", - extractContextualizedEmbeddingInput, - summarizeContextualizedEmbeddingOutput, - extractEmbeddingUsageMetrics, - ), - ); - } - - protected onDisable(): void { - this.unsubscribers = unsubscribeAll(this.unsubscribers); - } +export function registerVoyageAIInstrumentation(): void { + interceptVoyageAICall( + voyageAIChannels.embed, + "voyageai.embed", + extractTextEmbeddingInput, + summarizeEmbeddingOutput, + extractEmbeddingUsageMetrics, + ); + interceptVoyageAICall( + voyageAIChannels.multimodalEmbed, + "voyageai.multimodalEmbed", + extractMultimodalEmbeddingInput, + summarizeEmbeddingOutput, + extractEmbeddingUsageMetrics, + ); + interceptVoyageAICall( + voyageAIChannels.rerank, + "voyageai.rerank", + extractRerankInput, + summarizeRerankOutput, + ); + interceptVoyageAICall( + voyageAIChannels.contextualizedEmbed, + "voyageai.contextualizedEmbed", + extractContextualizedEmbeddingInput, + summarizeContextualizedEmbeddingOutput, + extractEmbeddingUsageMetrics, + ); } type VoyageAIResult = @@ -455,13 +445,10 @@ function extractUsageMetrics(result: VoyageAIResult): Record { const rawResponse = isObject(result.rawResponse) ? result.rawResponse : undefined; - const usage = ( - isObject(result.usage) - ? result.usage - : isObject(rawResponse?.usage) - ? rawResponse.usage - : undefined - ) as VoyageAIUsage | undefined; + const usageValue = isObject(result.usage) ? result.usage : rawResponse?.usage; + const usage = isObject(usageValue) + ? (usageValue as VoyageAIUsage) + : undefined; const tokens = typeof result.totalTokens === "number" ? result.totalTokens diff --git a/js/src/instrumentation/registry.test.ts b/js/src/instrumentation/registry.test.ts index fdd49a5c2..97d325cea 100644 --- a/js/src/instrumentation/registry.test.ts +++ b/js/src/instrumentation/registry.test.ts @@ -19,32 +19,25 @@ import iso from "../isomorph"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; -describe("Plugin Registry", () => { +describe("Instrumentation Registry", () => { beforeEach(() => { // Setup mock channel const mockChannel = { subscribe: vi.fn(), - unsubscribe: vi.fn(), hasSubscribers: false, }; mockNewTracingChannel.mockReturnValue(mockChannel); }); - // Clean up after each test - afterEach(() => { - registry.disable(); - }); - it("should not be enabled by default", () => { const testRegistry = new (registry.constructor as any)(); expect(testRegistry.isEnabled()).toBe(false); }); - it("should enable plugins when enable() is called", () => { + it("should enable instrumentation consumers when enable() is called", () => { const testRegistry = new (registry.constructor as any)(); testRegistry.enable(); expect(testRegistry.isEnabled()).toBe(true); - testRegistry.disable(); }); it("should be idempotent (calling enable() multiple times)", () => { @@ -52,12 +45,11 @@ describe("Plugin Registry", () => { testRegistry.enable(); testRegistry.enable(); // Should not throw expect(testRegistry.isEnabled()).toBe(true); - testRegistry.disable(); }); it("should block a second instance from subscribing when another is already enabled", () => { // Regression test for BT-5139: when the SDK is loaded from two different - // module paths in the same process, each gets its own PluginRegistry + // module paths in the same process, each gets its own instrumentation registry // instance. Without cross-instance deduplication, both would subscribe to // the same global hook, causing every OpenAI call to produce two // LLM spans. @@ -80,8 +72,6 @@ describe("Plugin Registry", () => { instanceB.enable(); expect(instanceB.isEnabled()).toBe(false); } finally { - instanceA.disable(); - instanceB.disable(); delete (globalThis as any)[stateKey]; } }); @@ -97,7 +87,6 @@ describe("Plugin Registry", () => { testRegistry.enable(); expect(testRegistry.isEnabled()).toBe(true); } finally { - testRegistry.disable(); delete (globalThis as any)[stateKey]; } }); @@ -116,7 +105,6 @@ describe("Plugin Registry", () => { expect(warnSpy[0]).toContain("Cannot configure instrumentation"); } finally { console.warn = originalWarn; - testRegistry.disable(); } }); @@ -133,33 +121,11 @@ describe("Plugin Registry", () => { expect(warnSpy.length).toBe(0); } finally { console.warn = originalWarn; - testRegistry.disable(); } }); - - it("should disable plugins when disable() is called", () => { - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - expect(testRegistry.isEnabled()).toBe(true); - - testRegistry.disable(); - expect(testRegistry.isEnabled()).toBe(false); - }); - - it("should be idempotent (calling disable() multiple times)", () => { - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - testRegistry.disable(); - testRegistry.disable(); // Should not throw - expect(testRegistry.isEnabled()).toBe(false); - }); }); describe("configureInstrumentation API", () => { - afterEach(() => { - registry.disable(); - }); - it("should export configureInstrumentation function", () => { expect(typeof configureInstrumentation).toBe("function"); }); @@ -195,7 +161,6 @@ describe("Environment Variable Configuration", () => { afterEach(async () => { const iso = (await import("../isomorph")).default; iso.getEnv = originalGetEnv; - registry.disable(); }); it("should parse BRAINTRUST_DISABLE_INSTRUMENTATION with single SDK", async () => { @@ -212,7 +177,6 @@ describe("Environment Variable Configuration", () => { // OpenAI should be disabled, others enabled by default expect(testRegistry.isEnabled()).toBe(true); - testRegistry.disable(); }); it("should parse BRAINTRUST_DISABLE_INSTRUMENTATION with multiple SDKs", async () => { @@ -229,7 +193,6 @@ describe("Environment Variable Configuration", () => { // Both should be disabled expect(testRegistry.isEnabled()).toBe(true); - testRegistry.disable(); }); it("should handle whitespace in BRAINTRUST_DISABLE_INSTRUMENTATION", async () => { @@ -245,7 +208,6 @@ describe("Environment Variable Configuration", () => { testRegistry.enable(); expect(testRegistry.isEnabled()).toBe(true); - testRegistry.disable(); }); it("should handle empty BRAINTRUST_DISABLE_INSTRUMENTATION", async () => { @@ -262,7 +224,6 @@ describe("Environment Variable Configuration", () => { // All should be enabled (nothing disabled) expect(testRegistry.isEnabled()).toBe(true); - testRegistry.disable(); }); it("should be case-insensitive for SDK names", async () => { @@ -278,6 +239,5 @@ describe("Environment Variable Configuration", () => { testRegistry.enable(); expect(testRegistry.isEnabled()).toBe(true); - testRegistry.disable(); }); }); diff --git a/js/src/instrumentation/registry.ts b/js/src/instrumentation/registry.ts index 0ed195ae8..010aee634 100644 --- a/js/src/instrumentation/registry.ts +++ b/js/src/instrumentation/registry.ts @@ -1,11 +1,11 @@ /** - * Plugin registry and configuration for auto-instrumentation. + * Registry and configuration for auto-instrumentation. * - * Plugins are automatically enabled when the Braintrust library is loaded. + * Instrumentation consumers are automatically enabled when Braintrust loads. * Users can disable specific integrations programmatically or via environment variables. */ -import { BraintrustPlugin } from "./braintrust-plugin"; +import { registerInstrumentationConsumers } from "./instrumentation-consumers"; import iso from "../isomorph"; import { getDefaultInstrumentationIntegrations, @@ -16,7 +16,7 @@ import { GLOBAL_INSTRUMENTATION_HOOKS_PROTOCOL_VERSION } from "../global-instrum export type { InstrumentationConfig } from "./config"; -// Key used to stamp the active PluginRegistry instance onto the shared +// Key used to stamp the active InstrumentationRegistry instance onto the shared // braintrust state object (globalThis[Symbol.for("braintrust-state")]). // // The braintrust state is already shared across all SDK instances loaded in @@ -43,8 +43,7 @@ function getSharedState(): Record | undefined { : undefined; } -class PluginRegistry { - private braintrustPlugin: BraintrustPlugin | null = null; +class InstrumentationRegistry { private config: InstrumentationConfig = {}; private enabled = false; @@ -65,7 +64,7 @@ class PluginRegistry { } /** - * Enable all configured plugins. + * Enable all configured instrumentation consumers. * Called automatically when the library is loaded. */ enable(): void { @@ -73,7 +72,7 @@ class PluginRegistry { return; } - // If another SDK instance in the same process already registered plugins, + // If another SDK instance already registered instrumentation consumers, // skip to avoid duplicate global hook subscriptions. const sharedState = getSharedState(); if (sharedState) { @@ -95,31 +94,8 @@ class PluginRegistry { }, }; - // Enable BraintrustPlugin with configuration - this.braintrustPlugin = new BraintrustPlugin(finalConfig); - this.braintrustPlugin.enable(); - } - - /** - * Disable all plugins. - * Primarily used for testing. - */ - disable(): void { - if (!this.enabled) { - return; - } - - this.enabled = false; - - const sharedState = getSharedState(); - if (sharedState && sharedState[REGISTRY_STATE_KEY] === this) { - delete sharedState[REGISTRY_STATE_KEY]; - } - - if (this.braintrustPlugin) { - this.braintrustPlugin.disable(); - this.braintrustPlugin = null; - } + // Enable the configured instrumentation consumers. + registerInstrumentationConsumers(finalConfig); } /** @@ -148,9 +124,9 @@ class PluginRegistry { } /** - * Global plugin registry instance. + * Global instrumentation registry instance. */ -export const registry = new PluginRegistry(); +export const registry = new InstrumentationRegistry(); /** * Configure auto-instrumentation. diff --git a/js/src/wrappers/ai-sdk/ai-sdk.ts b/js/src/wrappers/ai-sdk/ai-sdk.ts index 63de58ea6..dc6c7b09d 100644 --- a/js/src/wrappers/ai-sdk/ai-sdk.ts +++ b/js/src/wrappers/ai-sdk/ai-sdk.ts @@ -4,7 +4,7 @@ import { SpanTypeAttribute } from "../../../util"; import { aiSDKChannels, harnessAgentChannels, -} from "../../instrumentation/plugins/ai-sdk-channels"; +} from "../../instrumentation/providers/ai-sdk-channels"; import type { AISDK, AISDKAgentClass, diff --git a/js/src/wrappers/ai-sdk/harness-agent-context.ts b/js/src/wrappers/ai-sdk/harness-agent-context.ts index fc0b6171a..f17f2a442 100644 --- a/js/src/wrappers/ai-sdk/harness-agent-context.ts +++ b/js/src/wrappers/ai-sdk/harness-agent-context.ts @@ -335,10 +335,10 @@ export function currentHarnessTurnParent(): HarnessTurnParent | undefined { export function bindHarnessTurnParentToStart( tracingChannel: IsoTracingChannel, parentFromEvent: (event: T) => HarnessTurnParent | undefined, -): () => void { +): void { const startChannel = tracingChannel.start; if (!startChannel) { - return () => {}; + return; } harnessTurnParentStore ??= iso.newAsyncLocalStorage< @@ -349,9 +349,6 @@ export function bindHarnessTurnParentToStart( store, (event) => parentFromEvent(event) ?? store.getStore(), ); - return () => { - startChannel.unbindStore(store); - }; } export function startHarnessTurnChildSpan( diff --git a/js/src/wrappers/ai-sdk/telemetry.ts b/js/src/wrappers/ai-sdk/telemetry.ts index 1fb2c9812..110f8e221 100644 --- a/js/src/wrappers/ai-sdk/telemetry.ts +++ b/js/src/wrappers/ai-sdk/telemetry.ts @@ -20,7 +20,7 @@ import { processAISDKWorkflowAgentCallInput, processAISDKWorkflowAgentModelCallInput, serializeModelWithProvider, -} from "../../instrumentation/plugins/ai-sdk-plugin"; +} from "../../instrumentation/providers/ai-sdk-instrumentation"; import type { AISDKCallParams, AISDKEmbeddingResult, diff --git a/js/src/wrappers/anthropic.ts b/js/src/wrappers/anthropic.ts index 12aae0218..e4cfc708f 100644 --- a/js/src/wrappers/anthropic.ts +++ b/js/src/wrappers/anthropic.ts @@ -1,4 +1,4 @@ -import { anthropicChannels } from "../instrumentation/plugins/anthropic-channels"; +import { anthropicChannels } from "../instrumentation/providers/anthropic-channels"; import { TypedApplyProxy } from "../typed-instrumentation-helpers"; import type { AnthropicBeta, diff --git a/js/src/wrappers/bedrock-runtime.ts b/js/src/wrappers/bedrock-runtime.ts index 13fcd1844..4e60fd912 100644 --- a/js/src/wrappers/bedrock-runtime.ts +++ b/js/src/wrappers/bedrock-runtime.ts @@ -1,9 +1,9 @@ import { runWithAutoInstrumentationSuppressed } from "../instrumentation/auto-instrumentation-suppression"; -import { bedrockRuntimeChannels } from "../instrumentation/plugins/bedrock-runtime-channels"; +import { bedrockRuntimeChannels } from "../instrumentation/providers/bedrock-runtime-channels"; import { buildBedrockRuntimeSpanInfo, getBedrockRuntimeOperation, -} from "../instrumentation/plugins/bedrock-runtime-common"; +} from "../instrumentation/providers/bedrock-runtime-common"; import type { BedrockRuntimeClient, BedrockRuntimeCommandLike, diff --git a/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts b/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts index db24f0bcf..fae63e64d 100644 --- a/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts +++ b/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts @@ -1,6 +1,6 @@ -import { claudeAgentSDKChannels } from "../../instrumentation/plugins/claude-agent-sdk-channels"; -import { CLAUDE_AGENT_SDK_SKIP_LOCAL_TOOL_HOOKS_OPTION } from "../../instrumentation/plugins/claude-agent-sdk-instrumentation-constants"; -import { wrapLocalClaudeToolHandler } from "../../instrumentation/plugins/claude-agent-sdk-local-tool-spans"; +import { claudeAgentSDKChannels } from "../../instrumentation/providers/claude-agent-sdk-channels"; +import { CLAUDE_AGENT_SDK_SKIP_LOCAL_TOOL_HOOKS_OPTION } from "../../instrumentation/providers/claude-agent-sdk-instrumentation-constants"; +import { wrapLocalClaudeToolHandler } from "../../instrumentation/providers/claude-agent-sdk-local-tool-spans"; import type { ClaudeAgentSDKModule, ClaudeAgentSDKQueryParams, diff --git a/js/src/wrappers/cloudflare-agent.ts b/js/src/wrappers/cloudflare-agent.ts index c34bb6134..baee1d63d 100644 --- a/js/src/wrappers/cloudflare-agent.ts +++ b/js/src/wrappers/cloudflare-agent.ts @@ -1,5 +1,5 @@ import { debugLogger } from "../debug-logger"; -import { cloudflareAgentsChannels } from "../instrumentation/plugins/cloudflare-agents-channels"; +import { cloudflareAgentsChannels } from "../instrumentation/providers/cloudflare-agents-channels"; import type { CloudflareAgent } from "../vendor-sdk-types/cloudflare-agents"; const WRAPPED_RUN_AGENT_TOOL = Symbol.for( diff --git a/js/src/wrappers/cloudflare-ai-chat.ts b/js/src/wrappers/cloudflare-ai-chat.ts index 87df27775..11f834feb 100644 --- a/js/src/wrappers/cloudflare-ai-chat.ts +++ b/js/src/wrappers/cloudflare-ai-chat.ts @@ -1,5 +1,5 @@ import { debugLogger } from "../debug-logger"; -import { instrumentCloudflareAIChatAgent } from "../instrumentation/plugins/cloudflare-ai-chat-instrumentation"; +import { instrumentCloudflareAIChatAgent } from "../instrumentation/providers/cloudflare-ai-chat-instrumentation"; import type { CloudflareAIChatAgent, CloudflareAIChatAgentConstructor, diff --git a/js/src/wrappers/cloudflare-think.ts b/js/src/wrappers/cloudflare-think.ts index 9c04400ce..2b1e371bd 100644 --- a/js/src/wrappers/cloudflare-think.ts +++ b/js/src/wrappers/cloudflare-think.ts @@ -1,4 +1,4 @@ -import { cloudflareThinkChannels } from "../instrumentation/plugins/cloudflare-think-channels"; +import { cloudflareThinkChannels } from "../instrumentation/providers/cloudflare-think-channels"; import type { CloudflareThinkConstructor, CloudflareThinkInstance, diff --git a/js/src/wrappers/cohere.ts b/js/src/wrappers/cohere.ts index aa6038030..6c55f65dd 100644 --- a/js/src/wrappers/cohere.ts +++ b/js/src/wrappers/cohere.ts @@ -1,4 +1,4 @@ -import { cohereChannels } from "../instrumentation/plugins/cohere-channels"; +import { cohereChannels } from "../instrumentation/providers/cohere-channels"; import type { CohereChatRequest, CohereChatResponse, diff --git a/js/src/wrappers/cursor-sdk.ts b/js/src/wrappers/cursor-sdk.ts index e20f41fba..c20897b82 100644 --- a/js/src/wrappers/cursor-sdk.ts +++ b/js/src/wrappers/cursor-sdk.ts @@ -1,4 +1,4 @@ -import { cursorSDKChannels } from "../instrumentation/plugins/cursor-sdk-channels"; +import { cursorSDKChannels } from "../instrumentation/providers/cursor-sdk-channels"; import type { CursorSDKAgent, CursorSDKAgentClass, diff --git a/js/src/wrappers/genkit.test.ts b/js/src/wrappers/genkit.test.ts index 5363a64eb..3677dc99d 100644 --- a/js/src/wrappers/genkit.test.ts +++ b/js/src/wrappers/genkit.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { IsoChannelHandlers } from "../isomorph"; import type { ChannelMessage } from "../instrumentation/core/channel-definitions"; -import { genkitChannels } from "../instrumentation/plugins/genkit-channels"; +import { genkitChannels } from "../instrumentation/providers/genkit-channels"; import { configureNode } from "../node/config"; import type { GenkitAction, diff --git a/js/src/wrappers/genkit.ts b/js/src/wrappers/genkit.ts index fc35a8426..a4900ae9f 100644 --- a/js/src/wrappers/genkit.ts +++ b/js/src/wrappers/genkit.ts @@ -1,4 +1,4 @@ -import { genkitChannels } from "../instrumentation/plugins/genkit-channels"; +import { genkitChannels } from "../instrumentation/providers/genkit-channels"; import type { GenkitAction, GenkitEmbedManyParams, diff --git a/js/src/wrappers/github-copilot.ts b/js/src/wrappers/github-copilot.ts index 35e68fa87..20e76dbda 100644 --- a/js/src/wrappers/github-copilot.ts +++ b/js/src/wrappers/github-copilot.ts @@ -1,4 +1,4 @@ -import { gitHubCopilotChannels } from "../instrumentation/plugins/github-copilot-channels"; +import { gitHubCopilotChannels } from "../instrumentation/providers/github-copilot-channels"; import type { GitHubCopilotClient, GitHubCopilotResumeSessionConfig, diff --git a/js/src/wrappers/google-adk.ts b/js/src/wrappers/google-adk.ts index 28af7395e..6e908222e 100644 --- a/js/src/wrappers/google-adk.ts +++ b/js/src/wrappers/google-adk.ts @@ -1,4 +1,4 @@ -import { googleADKChannels } from "../instrumentation/plugins/google-adk-channels"; +import { googleADKChannels } from "../instrumentation/providers/google-adk-channels"; import type { GoogleADKRunner, GoogleADKRunnerConstructor, diff --git a/js/src/wrappers/google-genai.ts b/js/src/wrappers/google-genai.ts index 192f79c1a..7d21e8eea 100644 --- a/js/src/wrappers/google-genai.ts +++ b/js/src/wrappers/google-genai.ts @@ -1,4 +1,4 @@ -import { googleGenAIChannels } from "../instrumentation/plugins/google-genai-channels"; +import { googleGenAIChannels } from "../instrumentation/providers/google-genai-channels"; import { isObject } from "../util"; import type { GoogleGenAIClient, diff --git a/js/src/wrappers/groq.ts b/js/src/wrappers/groq.ts index 68312fa81..c736e00c8 100644 --- a/js/src/wrappers/groq.ts +++ b/js/src/wrappers/groq.ts @@ -1,4 +1,4 @@ -import { groqChannels } from "../instrumentation/plugins/groq-channels"; +import { groqChannels } from "../instrumentation/providers/groq-channels"; import type { GroqChat, GroqChatCompletion, diff --git a/js/src/wrappers/huggingface-transformers.ts b/js/src/wrappers/huggingface-transformers.ts index 3965f1234..52d8e2d56 100644 --- a/js/src/wrappers/huggingface-transformers.ts +++ b/js/src/wrappers/huggingface-transformers.ts @@ -1,7 +1,7 @@ import { huggingFaceTransformersChannels, isSupportedHuggingFaceTransformersTask, -} from "../instrumentation/plugins/huggingface-transformers-channels"; +} from "../instrumentation/providers/huggingface-transformers-channels"; import type { HuggingFaceTransformersModule, HuggingFaceTransformersPipeline, diff --git a/js/src/wrappers/huggingface.ts b/js/src/wrappers/huggingface.ts index 6ff98a259..13a58f515 100644 --- a/js/src/wrappers/huggingface.ts +++ b/js/src/wrappers/huggingface.ts @@ -1,4 +1,4 @@ -import { huggingFaceChannels } from "../instrumentation/plugins/huggingface-channels"; +import { huggingFaceChannels } from "../instrumentation/providers/huggingface-channels"; import { isObject } from "../../util"; import type { HuggingFaceChatCompletion, diff --git a/js/src/wrappers/langsmith.ts b/js/src/wrappers/langsmith.ts index a8486c573..983f42dd8 100644 --- a/js/src/wrappers/langsmith.ts +++ b/js/src/wrappers/langsmith.ts @@ -1,5 +1,5 @@ import { debugLogger } from "../debug-logger"; -import { langSmithChannels } from "../instrumentation/plugins/langsmith-channels"; +import { langSmithChannels } from "../instrumentation/providers/langsmith-channels"; import type { LangSmithClient, LangSmithClientConstructor, diff --git a/js/src/wrappers/mistral.ts b/js/src/wrappers/mistral.ts index c449997f7..4771dda38 100644 --- a/js/src/wrappers/mistral.ts +++ b/js/src/wrappers/mistral.ts @@ -1,4 +1,4 @@ -import { mistralChannels } from "../instrumentation/plugins/mistral-channels"; +import { mistralChannels } from "../instrumentation/providers/mistral-channels"; import type { MistralAgents, MistralAgentsCompletionResponse, diff --git a/js/src/wrappers/oai.ts b/js/src/wrappers/oai.ts index b2fa0850e..743460b7e 100644 --- a/js/src/wrappers/oai.ts +++ b/js/src/wrappers/oai.ts @@ -10,7 +10,7 @@ import type { ArgsOf, ResultOf, } from "../instrumentation/core/channel-definitions"; -import { openAIChannels } from "../instrumentation/plugins/openai-channels"; +import { openAIChannels } from "../instrumentation/providers/openai-channels"; import type { OpenAIChatCompletion, OpenAIChatCreateParams, diff --git a/js/src/wrappers/oai_responses.ts b/js/src/wrappers/oai_responses.ts index 8e936ebbc..432b1281a 100644 --- a/js/src/wrappers/oai_responses.ts +++ b/js/src/wrappers/oai_responses.ts @@ -3,7 +3,7 @@ import type { ResultOf, } from "../instrumentation/core/channel-definitions"; import type { ChannelSpanInfo } from "../instrumentation/core/types"; -import { openAIChannels } from "../instrumentation/plugins/openai-channels"; +import { openAIChannels } from "../instrumentation/providers/openai-channels"; import { parseMetricsFromUsage } from "../openai-utils"; import { APIPromise, diff --git a/js/src/wrappers/ollama.test.ts b/js/src/wrappers/ollama.test.ts index 9a8ebff46..b07dac5f4 100644 --- a/js/src/wrappers/ollama.test.ts +++ b/js/src/wrappers/ollama.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { debugLogger } from "../debug-logger"; -import { ollamaChannels } from "../instrumentation/plugins/ollama-channels"; +import { ollamaChannels } from "../instrumentation/providers/ollama-channels"; import type { OllamaClient } from "../vendor-sdk-types/ollama"; import { wrapOllama } from "./ollama"; diff --git a/js/src/wrappers/ollama.ts b/js/src/wrappers/ollama.ts index 3f90067a9..bcc48a937 100644 --- a/js/src/wrappers/ollama.ts +++ b/js/src/wrappers/ollama.ts @@ -1,5 +1,5 @@ import { debugLogger } from "../debug-logger"; -import { ollamaChannels } from "../instrumentation/plugins/ollama-channels"; +import { ollamaChannels } from "../instrumentation/providers/ollama-channels"; import { isObject } from "../../util"; import type { OllamaChatRequest, diff --git a/js/src/wrappers/openai-codex.ts b/js/src/wrappers/openai-codex.ts index 8b5a435b3..c4225e9c0 100644 --- a/js/src/wrappers/openai-codex.ts +++ b/js/src/wrappers/openai-codex.ts @@ -1,4 +1,4 @@ -import { openAICodexChannels } from "../instrumentation/plugins/openai-codex-channels"; +import { openAICodexChannels } from "../instrumentation/providers/openai-codex-channels"; import type { OpenAICodexClass, OpenAICodexClient, diff --git a/js/src/wrappers/openai-promise-utils.ts b/js/src/wrappers/openai-promise-utils.ts index afa23586c..b41e57f4b 100644 --- a/js/src/wrappers/openai-promise-utils.ts +++ b/js/src/wrappers/openai-promise-utils.ts @@ -6,7 +6,7 @@ import type { OpenAIAsyncChannel, OpenAIChannel, OpenAIStartContext, -} from "../instrumentation/plugins/openai-channels"; +} from "../instrumentation/providers/openai-channels"; export type EnhancedResponse = { response: Response; diff --git a/js/src/wrappers/openrouter-agent.test.ts b/js/src/wrappers/openrouter-agent.test.ts index 02f01e040..c1c94e12f 100644 --- a/js/src/wrappers/openrouter-agent.test.ts +++ b/js/src/wrappers/openrouter-agent.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { wrapOpenRouterAgent } from "./openrouter-agent"; -import { openRouterAgentChannels } from "../instrumentation/plugins/openrouter-agent-channels"; +import { openRouterAgentChannels } from "../instrumentation/providers/openrouter-agent-channels"; describe("wrapOpenRouterAgent", () => { afterEach(() => { diff --git a/js/src/wrappers/openrouter-agent.ts b/js/src/wrappers/openrouter-agent.ts index 07328249b..336b2d84f 100644 --- a/js/src/wrappers/openrouter-agent.ts +++ b/js/src/wrappers/openrouter-agent.ts @@ -1,4 +1,4 @@ -import { openRouterAgentChannels } from "../instrumentation/plugins/openrouter-agent-channels"; +import { openRouterAgentChannels } from "../instrumentation/providers/openrouter-agent-channels"; import type { OpenRouterAgentClient, OpenRouterAgentCallModelRequest, diff --git a/js/src/wrappers/openrouter.ts b/js/src/wrappers/openrouter.ts index bfd39d2c1..ab7231b0c 100644 --- a/js/src/wrappers/openrouter.ts +++ b/js/src/wrappers/openrouter.ts @@ -1,4 +1,4 @@ -import { openRouterChannels } from "../instrumentation/plugins/openrouter-channels"; +import { openRouterChannels } from "../instrumentation/providers/openrouter-channels"; import type { OpenRouterBeta, OpenRouterCallModelRequest, diff --git a/js/src/wrappers/pi-coding-agent.ts b/js/src/wrappers/pi-coding-agent.ts index 60565c596..f01f6a75a 100644 --- a/js/src/wrappers/pi-coding-agent.ts +++ b/js/src/wrappers/pi-coding-agent.ts @@ -1,4 +1,4 @@ -import { piCodingAgentChannels } from "../instrumentation/plugins/pi-coding-agent-channels"; +import { piCodingAgentChannels } from "../instrumentation/providers/pi-coding-agent-channels"; import type { PiAgentSession, PiAgentSessionClass, diff --git a/js/src/wrappers/strands-agent-sdk.ts b/js/src/wrappers/strands-agent-sdk.ts index 6c6173034..3b975c75c 100644 --- a/js/src/wrappers/strands-agent-sdk.ts +++ b/js/src/wrappers/strands-agent-sdk.ts @@ -1,4 +1,4 @@ -import { strandsAgentSDKChannels } from "../instrumentation/plugins/strands-agent-sdk-channels"; +import { strandsAgentSDKChannels } from "../instrumentation/providers/strands-agent-sdk-channels"; import type { StrandsAgent, StrandsAgentConstructor, diff --git a/js/src/wrappers/voyageai.ts b/js/src/wrappers/voyageai.ts index 6abc4bb1a..8285282f5 100644 --- a/js/src/wrappers/voyageai.ts +++ b/js/src/wrappers/voyageai.ts @@ -1,4 +1,4 @@ -import { voyageAIChannels } from "../instrumentation/plugins/voyageai-channels"; +import { voyageAIChannels } from "../instrumentation/providers/voyageai-channels"; import type { VoyageAIClient, VoyageAIContextualizedEmbedRequest, From 34bff9d12fda835815f06c76c0d9cf998c7384b1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 31 Aug 2026 20:05:47 +0200 Subject: [PATCH 07/19] ref!: Remove `flushAsync` option (#2409) Ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) `flushAsync` grinded my gears a bit a) as a user b) as a maintainer. Why?: - As a user, it added a quite intransparent API that is hard to wrap your head around. - As a user, it added a generic type to otherwise very non-generic APIs that made the LSP types incredibly hard to navigate - As a maintainer, it required a generic type to be passed around to literally everywhere - As a maintainer, it required us to have a weird `flushAsync` option to every tracing API In reality, what you can do instead, and which is very simple to understand, is simply do `await flush()` or `await logger.flush()` right after a span is ended. Not hard, very few if no drawbacks. --- .changeset/remove-flush-async.md | 5 + e2e/helpers/prod-forwarding.ts | 1 - .../src/BraintrustCallbackHandler.ts | 15 +- integrations/openai-agents-js/src/index.ts | 2 +- .../src/openai-agents-integration.test.ts | 2 +- integrations/openai-agents-js/src/types.ts | 2 +- js/src/exports.ts | 3 - .../providers/eve-instrumentation.ts | 4 +- .../openai-agents-trace-processor.ts | 4 +- js/src/logger.test.ts | 70 ++- js/src/logger.ts | 410 ++++++------------ js/src/vendor-sdk-types/langchain.ts | 4 +- js/src/wrappers/langchain/callback-handler.ts | 10 +- js/src/wrappers/test_oai_attachments.test.ts | 1 - .../api-compatibility.test.ts | 28 +- 15 files changed, 230 insertions(+), 331 deletions(-) create mode 100644 .changeset/remove-flush-async.md diff --git a/.changeset/remove-flush-async.md b/.changeset/remove-flush-async.md new file mode 100644 index 000000000..18903d6a9 --- /dev/null +++ b/.changeset/remove-flush-async.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref!: Remove `flushAsync` option diff --git a/e2e/helpers/prod-forwarding.ts b/e2e/helpers/prod-forwarding.ts index 522aedbe6..37f08d14f 100644 --- a/e2e/helpers/prod-forwarding.ts +++ b/e2e/helpers/prod-forwarding.ts @@ -31,7 +31,6 @@ export async function initializeProdForwarding(): Promise { const logger = initLogger({ apiKey, appUrl: process.env.BRAINTRUST_APP_URL, - asyncFlush: false, forceLogin: true, projectName, }); diff --git a/integrations/langchain-js/src/BraintrustCallbackHandler.ts b/integrations/langchain-js/src/BraintrustCallbackHandler.ts index d53b5ef69..56855154d 100644 --- a/integrations/langchain-js/src/BraintrustCallbackHandler.ts +++ b/integrations/langchain-js/src/BraintrustCallbackHandler.ts @@ -11,23 +11,18 @@ import { type LangChainCallbackHandlerOptions, } from "braintrust"; -type BraintrustCallbackHandlerOptions = - LangChainCallbackHandlerOptions; +type BraintrustCallbackHandlerOptions = LangChainCallbackHandlerOptions; /** * A Braintrust tracer for LangChain.js that logs LLM calls, chains, and tools. */ -export class BraintrustCallbackHandler< - IsAsyncFlush extends boolean, -> extends BaseCallbackHandler { +export class BraintrustCallbackHandler extends BaseCallbackHandler { name = "BraintrustCallbackHandler"; - private inner: BraintrustLangChainCallbackHandler; + private inner: BraintrustLangChainCallbackHandler; - constructor( - options?: Partial>, - ) { + constructor(options?: Partial) { super(); - this.inner = new BraintrustLangChainCallbackHandler(options); + this.inner = new BraintrustLangChainCallbackHandler(options); } handleLLMStart( diff --git a/integrations/openai-agents-js/src/index.ts b/integrations/openai-agents-js/src/index.ts index 394d61727..beafc3fc0 100644 --- a/integrations/openai-agents-js/src/index.ts +++ b/integrations/openai-agents-js/src/index.ts @@ -118,7 +118,7 @@ function getTimeElapsed(end?: string, start?: string): number | undefined { export class OpenAIAgentsTraceProcessor { private static readonly DEFAULT_MAX_TRACES = 10000; - private logger?: Logger; + private logger?: Logger; private maxTraces: number; private traceSpans = new Map< string, diff --git a/integrations/openai-agents-js/src/openai-agents-integration.test.ts b/integrations/openai-agents-js/src/openai-agents-integration.test.ts index 317d04b7e..71a6e5fd5 100644 --- a/integrations/openai-agents-js/src/openai-agents-integration.test.ts +++ b/integrations/openai-agents-js/src/openai-agents-integration.test.ts @@ -55,7 +55,7 @@ describe( TEST_SUITE_OPTIONS, () => { let backgroundLogger: TestBackgroundLogger; - let _logger: Logger; + let _logger: Logger; let Agent: any; let run: any; let tool: any; diff --git a/integrations/openai-agents-js/src/types.ts b/integrations/openai-agents-js/src/types.ts index 6834f5111..15aea9a6f 100644 --- a/integrations/openai-agents-js/src/types.ts +++ b/integrations/openai-agents-js/src/types.ts @@ -50,7 +50,7 @@ export type TraceMetadata = { }; export interface OpenAIAgentsTraceProcessorOptions { - logger?: Logger; + logger?: Logger; maxTraces?: number; } diff --git a/js/src/exports.ts b/js/src/exports.ts index 487ec7430..ffce08390 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -31,13 +31,11 @@ export type { InitLoggerOptions, InitOptions, LoadPromptOptions, - LogOptions, LoginOptions, Logs3OverflowInputRow, Logs3OverflowUpload, MetricSummary, ObjectMetadata, - PromiseUnless, PropagationContext, PromptRowWithId, ScoreSummary, @@ -125,7 +123,6 @@ export { withCurrent, withDataset, withExperiment, - withLogger, withParent, wrapTraced, registerOtelFlush, diff --git a/js/src/instrumentation/providers/eve-instrumentation.ts b/js/src/instrumentation/providers/eve-instrumentation.ts index 8729c6a48..610083878 100644 --- a/js/src/instrumentation/providers/eve-instrumentation.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.ts @@ -585,7 +585,7 @@ class EveBridge { turn: TurnState, event: { input?: unknown; output?: unknown }, ): void { - currentLogger()?.updateSpan({ + currentLogger()?.updateSpan({ id: turn.rowId, root_span_id: turn.rootSpanId, span_id: turn.spanId, @@ -637,7 +637,7 @@ class EveBridge { if (Object.keys(totals).length === 0) { return; } - currentLogger()?.updateSpan({ + currentLogger()?.updateSpan({ id: turn.rowId, metrics: totals, root_span_id: turn.rootSpanId, diff --git a/js/src/instrumentation/providers/openai-agents-trace-processor.ts b/js/src/instrumentation/providers/openai-agents-trace-processor.ts index e3116ed58..a335f2efb 100644 --- a/js/src/instrumentation/providers/openai-agents-trace-processor.ts +++ b/js/src/instrumentation/providers/openai-agents-trace-processor.ts @@ -44,7 +44,7 @@ type TraceMetadata = { }; export interface OpenAIAgentsTraceProcessorOptions { - logger?: Logger; + logger?: Logger; maxTraces?: number; } @@ -180,7 +180,7 @@ function parseUsageMetrics(usage: unknown): Record { export class OpenAIAgentsTraceProcessor { private static readonly DEFAULT_MAX_TRACES = 10000; - private logger?: Logger; + private logger?: Logger; private maxTraces: number; private traceSpans = new Map< string, diff --git a/js/src/logger.test.ts b/js/src/logger.test.ts index 86a3a1a5c..1dffdbb49 100644 --- a/js/src/logger.test.ts +++ b/js/src/logger.test.ts @@ -1,6 +1,14 @@ /* eslint-disable @typescript-eslint/consistent-type-assertions */ -import { vi, expect, test, describe, beforeEach, afterEach } from "vitest"; +import { + vi, + expect, + expectTypeOf, + test, + describe, + beforeEach, + afterEach, +} from "vitest"; import { _exportsForTestingOnly, init, @@ -11,6 +19,7 @@ import { loadPrompt, loadParameters, wrapTraced, + traced, currentSpan, withParent, startSpan, @@ -19,6 +28,8 @@ import { deepCopyEvent, ReadonlyExperiment, renderMessageImpl, + flush, + type InitLoggerOptions, } from "./logger"; import { configureNode } from "./node/config"; @@ -31,6 +42,12 @@ import { SpanCache } from "./span-cache"; configureNode(); +type InitLoggerHasAsyncFlush = "asyncFlush" extends keyof InitLoggerOptions + ? true + : false; + +expectTypeOf().toEqualTypeOf(); + test("renderMessage with file content parts", () => { const message = { role: "user" as const, @@ -426,6 +443,57 @@ test("verify MemoryBackgroundLogger intercepts logs", async () => { _exportsForTestingOnly.clearTestBackgroundLogger(); // can go back to normal }); +test("logger and tracing APIs preserve return shapes without implicit flushing", async () => { + await _exportsForTestingOnly.simulateLoginForTests(); + const backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); + const flushSpy = vi.spyOn(backgroundLogger, "flush"); + + try { + const logger = initLogger({ + projectName: "test", + projectId: "test-project-id", + }); + + const id = logger.log({ input: "input" }); + expectTypeOf(id).toEqualTypeOf(); + + const loggerSyncResult = logger.traced(() => 42 as const); + expectTypeOf(loggerSyncResult).toEqualTypeOf<42>(); + expect(loggerSyncResult).toBe(42); + + const loggerAsyncResult = logger.traced(async () => 43); + expectTypeOf(loggerAsyncResult).toEqualTypeOf>(); + await expect(loggerAsyncResult).resolves.toBe(43); + + const tracedSyncResult = traced(() => "sync" as const); + expectTypeOf(tracedSyncResult).toEqualTypeOf<"sync">(); + expect(tracedSyncResult).toBe("sync"); + + const syncFunction = (value: number) => value + 1; + const wrappedSyncFunction = wrapTraced(syncFunction); + expectTypeOf(wrappedSyncFunction).toEqualTypeOf(); + expect(wrappedSyncFunction(1)).toBe(2); + + const asyncFunction = async (value: number) => value + 1; + const wrappedAsyncFunction = wrapTraced(asyncFunction); + expectTypeOf(wrappedAsyncFunction).toEqualTypeOf(); + await expect(wrappedAsyncFunction(2)).resolves.toBe(3); + + expect(flushSpy).not.toHaveBeenCalled(); + + await logger.flush(); + const span = logger.startSpan({ name: "explicit-flush" }); + span.end(); + await span.flush(); + await flush(); + + expect(flushSpy).toHaveBeenCalledTimes(3); + } finally { + _exportsForTestingOnly.clearTestBackgroundLogger(); + _exportsForTestingOnly.simulateLogoutForTests(); + } +}); + test("init validation", () => { expect(() => init({})).toThrow( "Must specify at least one of project or projectId", diff --git a/js/src/logger.ts b/js/src/logger.ts index fcf88d9a4..793cb96fc 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -764,9 +764,7 @@ function normalizeProxyConnUrl(proxyUrl: string): string { export class BraintrustState { public id: string; public currentExperiment: Experiment | undefined; - // Note: the value of IsAsyncFlush doesn't really matter here, since we - // (safely) dynamically cast it whenever retrieving the logger. - public currentLogger: Logger | undefined; + public currentLogger: Logger | undefined; public currentParent: IsoAsyncLocalStorage; public currentSpan: IsoAsyncLocalStorage; // Any time we re-log in, we directly update the apiConn inside the logger. @@ -1531,20 +1529,15 @@ interface OrgProjectMetadata { project: ObjectMetadata; } -export interface LinkArgs { +interface LinkArgs { org_name?: string; app_url?: string; - project_name?: string; - project_id?: string; -} - -export interface LogOptions { - asyncFlush?: IsAsyncFlush; - computeMetadataArgs?: Record; - linkArgs?: LinkArgs; } -export type PromiseUnless = B extends true ? R : Promise>; +type ProjectMetadataArgs = { + project_name?: string; + project_id?: string; +}; export interface AttachmentParams { data: string | Blob | ArrayBuffer; @@ -2546,12 +2539,11 @@ function startSpanParentArgs(args: { }; } -export class Logger implements Exportable { +export class Logger implements Exportable { private state: BraintrustState; private lazyMetadata: LazyValue; - private _asyncFlush: IsAsyncFlush | undefined; - private computeMetadataArgs: Record | undefined; - private _linkArgs: LinkArgs | undefined; + private computeMetadataArgs: ProjectMetadataArgs | undefined; + private linkArgs: LinkArgs | undefined; private lastStartTime: number; private lazyId: LazyValue; private calledStartSpan: boolean; @@ -2562,14 +2554,16 @@ export class Logger implements Exportable { constructor( state: BraintrustState, lazyMetadata: LazyValue, - logOptions: LogOptions = {}, + options: { + computeMetadataArgs?: ProjectMetadataArgs; + linkArgs?: LinkArgs; + } = {}, ) { this.lazyMetadata = lazyMetadata; - this._asyncFlush = logOptions.asyncFlush; - this.computeMetadataArgs = logOptions.computeMetadataArgs; - this._linkArgs = logOptions.linkArgs; + this.computeMetadataArgs = options.computeMetadataArgs; + this.linkArgs = options.linkArgs; this.lastStartTime = getCurrentUnixTimestamp(); - this.lazyId = new LazyValue(async () => await this.id); + this.lazyId = new LazyValue(() => this.id); this.calledStartSpan = false; this.state = state; } @@ -2599,7 +2593,7 @@ export class Logger implements Exportable { } /** - * Log a single event. The event will be batched and uploaded behind the scenes if `logOptions.asyncFlush` is true. + * Log a single event. The event will be batched and uploaded behind the scenes. Call and await {@link Logger.flush} to ensure the event has been uploaded. * * @param event The event to log. * @param event.input: (Optional) the arguments that uniquely define a user input (an arbitrary, JSON serializable object). @@ -2617,7 +2611,7 @@ export class Logger implements Exportable { public log( event: Readonly, options?: { allowConcurrentWithSpans?: boolean }, - ): PromiseUnless { + ): string { if (this.calledStartSpan && !options?.allowConcurrentWithSpans) { throw new Error( "Cannot run toplevel `log` method while using spans. To log to the span, call `logger.traced` and then log with `span.log`", @@ -2626,16 +2620,7 @@ export class Logger implements Exportable { const span = this.startSpanImpl({ startTime: this.lastStartTime, event }); this.lastStartTime = span.end(); - const ret = span.id; - type Ret = PromiseUnless; - if (this.asyncFlush === true) { - return ret as Ret; - } else { - return (async () => { - await this.flush(); - return ret; - })() as Ret; - } + return span.id; } /** @@ -2646,11 +2631,11 @@ export class Logger implements Exportable { public traced( callback: (span: Span) => R, args?: StartSpanArgs & SetCurrentArg, - ): PromiseUnless { + ): R { const { setCurrent, ...argsRest } = args ?? {}; const span = this.startSpan(argsRest); - const ret = runCatchFinally( + return runCatchFinally( () => { if (setCurrent ?? true) { return withCurrent(span, callback); @@ -2658,23 +2643,12 @@ export class Logger implements Exportable { return callback(span); } }, - (e) => { - logError(span, e); - throw e; + (error) => { + logError(span, error); + throw error; }, () => span.end(), ); - type Ret = PromiseUnless; - - if (this.asyncFlush) { - return ret as Ret; - } else { - return (async () => { - const awaitedRet = await ret; - await this.flush(); - return awaitedRet; - })() as Ret; - } } /** @@ -2773,17 +2747,13 @@ export class Logger implements Exportable { return await this.state.bgLogger().flush(); } - get asyncFlush(): IsAsyncFlush | undefined { - return this._asyncFlush; - } - /** * Return the base URL for links (e.g. https://braintrust.dev/app/my-org-name) * if we have the info, otherwise return null. * Resolution order: state -> linkArgs -> env var */ public _getLinkBaseUrl(): string | null { - return _getLinkBaseUrl(this.state, this._linkArgs); + return _getLinkBaseUrl(this.state, this.linkArgs); } /** @@ -2805,19 +2775,6 @@ export class Logger implements Exportable { } } -function castLogger( - logger: Logger | undefined, - asyncFlush?: ToB, -): Logger | undefined { - if (logger === undefined) return undefined; - if (asyncFlush !== undefined && !!asyncFlush !== !!logger.asyncFlush) { - throw new Error( - `Asserted asyncFlush setting ${asyncFlush} does not match stored logger's setting ${logger.asyncFlush}`, - ); - } - return logger as unknown as Logger; -} - export type Logs3OverflowUpload = { method: "PUT" | "POST"; signedUrl: string; @@ -4210,22 +4167,6 @@ export function withExperiment( return callback(experiment); } -/** - * @deprecated Use {@link initLogger} instead. - */ -export function withLogger( - callback: (logger: Logger) => R, - options: Readonly & SetCurrentArg> = {}, -): R { - debugLogger - .forState(options.state) - .warn( - "withLogger is deprecated and will be removed in a future version of braintrust. Simply create the logger with `initLogger`.", - ); - const logger = initLogger(options); - return callback(logger); -} - type UseOutputOption = { useOutput?: IsLegacyDataset; }; @@ -4675,13 +4616,7 @@ export function withDataset< // from arguments serialized elsewhere. async function computeLoggerMetadata( state: BraintrustState, - { - project_name, - project_id, - }: { - project_name?: string; - project_id?: string; - }, + { project_name, project_id }: ProjectMetadataArgs, ) { await state.login({}); const org_id = state.orgId!; @@ -4718,18 +4653,13 @@ async function computeLoggerMetadata( } } -type AsyncFlushArg = { - asyncFlush?: IsAsyncFlush; -}; - -export type InitLoggerOptions = FullLoginOptions & { +export type InitLoggerOptions = FullLoginOptions & { projectName?: string; projectId?: string; environment?: SpanOriginEnvironment; setCurrent?: boolean; state?: BraintrustState; - orgProjectMetadata?: OrgProjectMetadata; -} & AsyncFlushArg; +}; /** * Create a new logger in a specified project. If the project does not exist, it will be created. @@ -4737,7 +4667,6 @@ export type InitLoggerOptions = FullLoginOptions & { * @param options Additional options for configuring init(). * @param options.projectName The name of the project to log into. If unspecified, will default to the Global project. * @param options.projectId The id of the project to log into. This takes precedence over projectName if specified. - * @param options.asyncFlush If true, will log asynchronously in the background. Otherwise, will log synchronously. (true by default) * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, * if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. @@ -4747,13 +4676,10 @@ export type InitLoggerOptions = FullLoginOptions & { * @param setCurrent If true (the default), set the global current-experiment to the newly-created one. * @returns The newly created Logger. */ -export function initLogger( - options: Readonly> = {}, -) { +export function initLogger(options: Readonly = {}): Logger { const { projectName, projectId, - asyncFlush: asyncFlushArg, appUrl, apiKey, orgName, @@ -4764,9 +4690,6 @@ export function initLogger( state: stateArg, } = options || {}; - const asyncFlush = - asyncFlushArg === undefined ? (true as IsAsyncFlush) : asyncFlushArg; - const computeMetadataArgs = { project_name: projectName, project_id: projectId, @@ -4775,8 +4698,6 @@ export function initLogger( const linkArgs = { org_name: orgName, app_url: appUrl, - project_name: projectName, - project_id: projectId, }; const state = stateArg ?? _globalState; @@ -4801,13 +4722,12 @@ export function initLogger( }, ); - const ret = new Logger(state, lazyMetadata, { - asyncFlush, + const ret = new Logger(state, lazyMetadata, { computeMetadataArgs, linkArgs, }); if (options.setCurrent ?? true) { - state.currentLogger = ret as Logger; + state.currentLogger = ret; } return ret; } @@ -5484,11 +5404,9 @@ export function currentExperiment( /** * Returns the currently-active logger (set by {@link initLogger}). Returns undefined if no current logger has been set. */ -export function currentLogger( - options?: AsyncFlushArg & OptionalStateArg, -): Logger | undefined { +export function currentLogger(options?: OptionalStateArg): Logger | undefined { const state = options?.state ?? _globalState; - return castLogger(state.currentLogger, options?.asyncFlush); + return state.currentLogger; } /** @@ -5511,16 +5429,15 @@ export function currentSpan(options?: OptionalStateArg): Span { * disagreeing if state changed between calls). The state is only meaningful when * a parent slug was resolved; otherwise it is undefined. */ -function getSpanParentObjectAndPropagatedState( - options?: AsyncFlushArg & - OptionalStateArg & { parent?: string | PropagationContext }, +function getSpanParentObjectAndPropagatedState( + options?: OptionalStateArg & { parent?: string | PropagationContext }, ): { parentObject: | SpanComponentsV3 | SpanComponentsV4 | Span | Experiment - | Logger; + | Logger; propagatedState: PropagatedState | undefined; } { const state = options?.state ?? _globalState; @@ -5539,11 +5456,11 @@ function getSpanParentObjectAndPropagatedState( }; } - const experiment = currentExperiment(); + const experiment = state.currentExperiment; if (experiment) { return { parentObject: experiment, propagatedState: undefined }; } - const logger = currentLogger(options); + const logger = state.currentLogger; if (logger) { return { parentObject: logger, propagatedState: undefined }; } @@ -5557,15 +5474,9 @@ function getSpanParentObjectAndPropagatedState( * `parent` may be an exported slug string or an opaque W3C trace-context (from * {@link extractTraceContextFromHeaders}). */ -export function getSpanParentObject( - options?: AsyncFlushArg & - OptionalStateArg & { parent?: string | PropagationContext }, -): - | SpanComponentsV3 - | SpanComponentsV4 - | Span - | Experiment - | Logger { +export function getSpanParentObject( + options?: OptionalStateArg & { parent?: string | PropagationContext }, +): SpanComponentsV3 | SpanComponentsV4 | Span | Experiment | Logger { return getSpanParentObjectAndPropagatedState(options).parentObject; } @@ -6063,45 +5974,33 @@ export function logError(span: Span, error: unknown) { * * See {@link Span.traced} for full details. */ -export function traced( +export function traced( callback: (span: Span) => R, - args?: StartSpanArgs & - SetCurrentArg & - AsyncFlushArg & - OptionalStateArg, -): PromiseUnless { - const { span, isSyncFlushLogger } = startSpanAndIsLogger(args); - - const ret = runCatchFinally( + args?: StartSpanArgs & SetCurrentArg & OptionalStateArg, +): R { + const { setCurrent, ...spanArgs } = args ?? {}; + const span = startSpanImpl(spanArgs); + + return runCatchFinally( () => { - if (args?.setCurrent ?? true) { + if (setCurrent ?? true) { return withCurrent(span, callback); } else { return callback(span); } }, - (e) => { - logError(span, e); - throw e; + (error) => { + logError(span, error); + throw error; }, () => span.end(), ); - - type Ret = PromiseUnless; - - if (args?.asyncFlush === undefined || args?.asyncFlush) { - return ret as Ret; - } else { - return (async () => { - const awaitedRet = await ret; - if (isSyncFlushLogger) { - await span.flush(); - } - return awaitedRet; - })() as Ret; - } } +type WrapTracedArgs = { + noTraceIO?: boolean; +}; + /** * Check if a function is a sync generator function. * @@ -6113,7 +6012,7 @@ export function traced( * @param fn The function to check. * @returns True if the function is a sync generator function. */ -function isGeneratorFunction(fn: any): boolean { +function isGeneratorFunction(fn: unknown): boolean { return Object.prototype.toString.call(fn) === "[object GeneratorFunction]"; } @@ -6124,7 +6023,7 @@ function isGeneratorFunction(fn: any): boolean { * @param fn The function to check. * @returns True if the function is an async generator function. */ -function isAsyncGeneratorFunction(fn: any): boolean { +function isAsyncGeneratorFunction(fn: unknown): boolean { return ( Object.prototype.toString.call(fn) === "[object AsyncGeneratorFunction]" ); @@ -6135,10 +6034,13 @@ function isAsyncGeneratorFunction(fn: any): boolean { */ function wrapTracedSyncGenerator any>( fn: F, - spanArgs: any, + spanArgs: StartSpanArgs & SetCurrentArg, noTraceIO: boolean, ): F { - const wrapper = function* (this: any, ...fnArgs: Parameters) { + const wrapper = function* ( + this: ThisParameterType, + ...fnArgs: Parameters + ) { const span = startSpan(spanArgs); try { if (!noTraceIO) { @@ -6149,7 +6051,7 @@ function wrapTracedSyncGenerator any>( const maxItems = envValue !== undefined ? Number(envValue) : 1000; if (!noTraceIO && maxItems !== 0) { - let collected: any[] = []; + let collected: unknown[] = []; let truncated = false; const gen = generatorWithCurrent(span, fn.apply(this, fnArgs)); @@ -6202,10 +6104,13 @@ function wrapTracedSyncGenerator any>( */ function wrapTracedAsyncGenerator any>( fn: F, - spanArgs: any, + spanArgs: StartSpanArgs & SetCurrentArg, noTraceIO: boolean, ): F { - const wrapper = async function* (this: any, ...fnArgs: Parameters) { + const wrapper = async function* ( + this: ThisParameterType, + ...fnArgs: Parameters + ) { const span = startSpan(spanArgs); try { if (!noTraceIO) { @@ -6216,7 +6121,7 @@ function wrapTracedAsyncGenerator any>( const maxItems = envValue !== undefined ? Number(envValue) : 1000; if (!noTraceIO && maxItems !== 0) { - let collected: any[] = []; + let collected: unknown[] = []; let truncated = false; const gen = asyncGeneratorWithCurrent(span, fn.apply(this, fnArgs)); @@ -6264,10 +6169,6 @@ function wrapTracedAsyncGenerator any>( return wrapper as F; } -type WrapTracedArgs = { - noTraceIO?: boolean; -}; - /** * Wrap a function with `traced`, using the arguments as `input` and return value as `output`. * Any functions wrapped this way will automatically be traced, similar to the `@traced` decorator @@ -6293,84 +6194,52 @@ type WrapTracedArgs = { * @param args Span-level arguments (e.g. a custom name or type) to pass to `traced`. * @returns The wrapped function. */ -export function wrapTraced< - F extends (...args: any[]) => any, - IsAsyncFlush extends boolean = true, ->( +export function wrapTraced any>( fn: F, - args?: StartSpanArgs & - SetCurrentArg & - AsyncFlushArg & - WrapTracedArgs, -): IsAsyncFlush extends false - ? (...args: Parameters) => Promise>> - : F { - const spanArgs: typeof args = { + args?: StartSpanArgs & SetCurrentArg & WrapTracedArgs, +): F { + const { noTraceIO, ...argsRest } = args ?? {}; + const spanArgs: StartSpanArgs & SetCurrentArg = { name: fn.name, type: "function", - ...args, + ...argsRest, }; - const hasExplicitInput = - args && - args.event && - "input" in args.event && - args.event.input !== undefined; - const hasExplicitOutput = - args && args.event && args.event.output !== undefined; - - const noTraceIO = args?.noTraceIO || hasExplicitInput || hasExplicitOutput; + const hasExplicitInput = spanArgs.event?.input !== undefined; + const hasExplicitOutput = spanArgs.event?.output !== undefined; + + const disableGeneratorTraceIO = + !!noTraceIO || hasExplicitInput || hasExplicitOutput; // Check if the function is a generator if (isGeneratorFunction(fn)) { - return wrapTracedSyncGenerator(fn, spanArgs, !!noTraceIO); + return wrapTracedSyncGenerator(fn, spanArgs, disableGeneratorTraceIO); } if (isAsyncGeneratorFunction(fn)) { - return wrapTracedAsyncGenerator(fn, spanArgs, !!noTraceIO); + return wrapTracedAsyncGenerator(fn, spanArgs, disableGeneratorTraceIO); } - if (args?.asyncFlush) { - return ((...fnArgs: Parameters) => - traced((span) => { - if (!hasExplicitInput) { - span.log({ input: fnArgs }); - } - - const output = fn(...fnArgs); - - if (!hasExplicitOutput) { - if (output instanceof Promise) { - return (async () => { - const result = await output; - span.log({ output: result }); - return result; - })(); - } else { - span.log({ output: output }); - } - } - - return output; - }, spanArgs)) as IsAsyncFlush extends false ? never : F; - } else { - return ((...fnArgs: Parameters) => - traced(async (span) => { - if (!hasExplicitInput) { - span.log({ input: fnArgs }); - } - - const outputResult = fn(...fnArgs); + return ((...fnArgs: Parameters) => + traced((span) => { + if (!noTraceIO && !hasExplicitInput) { + span.log({ input: fnArgs }); + } - const output = await outputResult; + const output = fn(...fnArgs); - if (!hasExplicitOutput) { + if (!noTraceIO && !hasExplicitOutput) { + if (output instanceof Promise) { + return (async () => { + const result = await output; + span.log({ output: result }); + return result; + })(); + } else { span.log({ output }); } + } - return output; - }, spanArgs)) as IsAsyncFlush extends false - ? (...args: Parameters) => Promise>> - : never; - } + return output; + }, spanArgs)) as F; } /** @@ -6386,36 +6255,29 @@ export const traceable = wrapTraced; * * See {@link traced} for full details. */ -export function startSpan( - args?: StartSpanArgs & AsyncFlushArg & OptionalStateArg, -): Span { - return startSpanAndIsLogger(args).span; +export function startSpan(args?: StartSpanArgs & OptionalStateArg): Span { + return startSpanImpl(args); } /** @internal Start a span whose initial row is merged with concurrent writes. */ -export function _internalStartSpanWithInitialMerge< - IsAsyncFlush extends boolean = true, ->(args?: StartSpanArgs & AsyncFlushArg & OptionalStateArg): Span { - return startSpanAndIsLogger({ +export function _internalStartSpanWithInitialMerge( + args?: StartSpanArgs & OptionalStateArg, +): Span { + return startSpanImpl({ ...args, [INITIAL_SPAN_WRITE_AS_MERGE]: true, - } as StartSpanArgs & - AsyncFlushArg & - OptionalStateArg & - InitialSpanWriteAsMergeArg).span; + } as StartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg); } /** @internal Start a span with SDK-controlled context fields. */ -export function _internalStartSpanWithContext< - IsAsyncFlush extends boolean = true, ->( - args: StartSpanArgs & AsyncFlushArg & OptionalStateArg, +export function _internalStartSpanWithContext( + args: StartSpanArgs & OptionalStateArg, context: Record, ): Span { - return startSpanAndIsLogger({ + return startSpanImpl({ ...args, [INTERNAL_SPAN_CONTEXT]: context, - }).span; + }); } /** @@ -6436,23 +6298,16 @@ export function setFetch(fetch: typeof globalThis.fetch): void { _internalGetGlobalState().setFetch(fetch); } -function startSpanAndIsLogger( - args?: StartSpanArgs & - AsyncFlushArg & - OptionalStateArg & - InternalSpanContextArg, -): { span: Span; isSyncFlushLogger: boolean } { +function startSpanImpl( + args?: StartSpanArgs & OptionalStateArg & InternalSpanContextArg, +): Span { const state = args?.state ?? _globalState; // Resolve the parent object and any forwarded W3C state in one pass, so we // don't re-normalize `parent` (which could disagree if the active // logger/experiment changed between calls). const { parentObject, propagatedState } = - getSpanParentObjectAndPropagatedState({ - asyncFlush: args?.asyncFlush, - parent: args?.parent, - state, - }); + getSpanParentObjectAndPropagatedState(args); if ( parentObject instanceof SpanComponentsV3 || @@ -6472,7 +6327,7 @@ function startSpanAndIsLogger( // The parent object/state are already resolved from `parent` above; drop // the raw `parent` so it isn't re-normalized. const { parent: _ignoredParent, ...spanArgs } = args ?? {}; - const span = new SpanImpl({ + return new SpanImpl({ state, ...spanArgs, parentObjectType: parentObject.data.object_type, @@ -6490,21 +6345,8 @@ function startSpanAndIsLogger( | undefined), propagatedState, }); - return { - span, - isSyncFlushLogger: - parentObject.data.object_type === SpanObjectTypeV3.PROJECT_LOGS && - // Since there's no parent logger here, we're free to choose the async flush - // behavior, and therefore propagate along whatever we get from the arguments - args?.asyncFlush === false, - }; } else { - const span = parentObject.startSpan(args); - return { - span, - isSyncFlushLogger: - parentObject.kind === "logger" && parentObject.asyncFlush === false, - }; + return parentObject.startSpan(args); } } @@ -7197,7 +7039,7 @@ export class Experiment this.lazyMetadata = lazyMetadata; this.dataset = dataset; this.lastStartTime = getCurrentUnixTimestamp(); - this.lazyId = new LazyValue(async () => await this.id); + this.lazyId = new LazyValue(() => this.id); this.calledStartSpan = false; this.state = state; } @@ -7298,7 +7140,7 @@ export class Experiment const { setCurrent, ...argsRest } = args ?? {}; const span = this.startSpan(argsRest); - const ret = runCatchFinally( + return runCatchFinally( () => { if (setCurrent ?? true) { return withCurrent(span, callback); @@ -7306,14 +7148,12 @@ export class Experiment return callback(span); } }, - (e) => { - logError(span, e); - throw e; + (error) => { + logError(span, error); + throw error; }, () => span.end(), ); - - return ret as R; } /** @@ -7941,9 +7781,9 @@ export class SpanImpl implements Span { return callback(span); } }, - (e) => { - logError(span, e); - throw e; + (error) => { + logError(span, error); + throw error; }, () => span.end(), ); diff --git a/js/src/vendor-sdk-types/langchain.ts b/js/src/vendor-sdk-types/langchain.ts index a4ffdc909..fd795b8b5 100644 --- a/js/src/vendor-sdk-types/langchain.ts +++ b/js/src/vendor-sdk-types/langchain.ts @@ -27,10 +27,10 @@ export type LangChainCallbackManagerConfigureArgs = [ options?: unknown, ]; -export type LangChainCallbackHandlerOptions = { +export type LangChainCallbackHandlerOptions = { debug: boolean; excludeMetadataProps: RegExp; - logger?: Logger | Span; + logger?: Logger | Span; parent?: Span | (() => Span); }; diff --git a/js/src/wrappers/langchain/callback-handler.ts b/js/src/wrappers/langchain/callback-handler.ts index a0143b338..51b073173 100644 --- a/js/src/wrappers/langchain/callback-handler.ts +++ b/js/src/wrappers/langchain/callback-handler.ts @@ -15,22 +15,18 @@ import type { export const BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME = "BraintrustCallbackHandler"; -export class BraintrustLangChainCallbackHandler< - IsAsyncFlush extends boolean = true, -> { +export class BraintrustLangChainCallbackHandler { name = BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME; private spans = new Map(); private skippedRuns = new Set(); private parent?: Span | (() => Span); private rootRunId?: string; - private options: LangChainCallbackHandlerOptions; + private options: LangChainCallbackHandlerOptions; private startTimes = new Map(); private firstTokenTimes = new Map(); private ttftMs = new Map(); - constructor( - options?: Partial>, - ) { + constructor(options?: Partial) { this.parent = options?.parent; this.options = { debug: options?.debug ?? false, diff --git a/js/src/wrappers/test_oai_attachments.test.ts b/js/src/wrappers/test_oai_attachments.test.ts index 6fcc0ec72..443086900 100644 --- a/js/src/wrappers/test_oai_attachments.test.ts +++ b/js/src/wrappers/test_oai_attachments.test.ts @@ -11,7 +11,6 @@ describe("OpenAI attachment processing", () => { initLogger({ projectName: PROJECT_NAME, apiKey: "test-key", - asyncFlush: false, }); }); diff --git a/js/tests/api-compatibility/api-compatibility.test.ts b/js/tests/api-compatibility/api-compatibility.test.ts index 2fd785357..3941302be 100644 --- a/js/tests/api-compatibility/api-compatibility.test.ts +++ b/js/tests/api-compatibility/api-compatibility.test.ts @@ -2223,8 +2223,8 @@ describe("isUnionTypeWidening", () => { describe("areInterfaceSignaturesCompatible", () => { test("should allow adding optional fields to interface", () => { - const oldInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; computeMetadataArgs?: Record; }`; - const newInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; computeMetadataArgs?: Record; linkArgs?: LinkArgs; }`; + const oldInterface = `export interface FeatureOptions { enabled?: Enabled; metadata?: Record; }`; + const newInterface = `export interface FeatureOptions { enabled?: Enabled; metadata?: Record; link?: string; }`; const result = areInterfaceSignaturesCompatible(oldInterface, newInterface); expect(result).toBe(true); @@ -2239,40 +2239,40 @@ describe("areInterfaceSignaturesCompatible", () => { }); test("should reject removing fields from interface", () => { - const oldInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; computeMetadataArgs?: Record; }`; - const newInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; }`; + const oldInterface = `export interface FeatureOptions { enabled?: Enabled; metadata?: Record; }`; + const newInterface = `export interface FeatureOptions { enabled?: Enabled; }`; const result = areInterfaceSignaturesCompatible(oldInterface, newInterface); expect(result).toBe(false); }); test("should reject adding required fields to interface", () => { - const oldInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; }`; - const newInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; requiredField: string; }`; + const oldInterface = `export interface FeatureOptions { enabled?: Enabled; }`; + const newInterface = `export interface FeatureOptions { enabled?: Enabled; requiredField: string; }`; const result = areInterfaceSignaturesCompatible(oldInterface, newInterface); expect(result).toBe(false); }); test("should reject changing field types in interface", () => { - const oldInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; computeMetadataArgs?: Record; }`; - const newInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; computeMetadataArgs?: string; }`; + const oldInterface = `export interface FeatureOptions { enabled?: Enabled; metadata?: Record; }`; + const newInterface = `export interface FeatureOptions { enabled?: Enabled; metadata?: string; }`; const result = areInterfaceSignaturesCompatible(oldInterface, newInterface); expect(result).toBe(false); }); test("should allow making required field optional", () => { - const oldInterface = `export interface LogOptions { asyncFlush: IsAsyncFlush; }`; - const newInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; }`; + const oldInterface = `export interface FeatureOptions { enabled: Enabled; }`; + const newInterface = `export interface FeatureOptions { enabled?: Enabled; }`; const result = areInterfaceSignaturesCompatible(oldInterface, newInterface); expect(result).toBe(true); }); test("should reject making optional field required", () => { - const oldInterface = `export interface LogOptions { asyncFlush?: IsAsyncFlush; }`; - const newInterface = `export interface LogOptions { asyncFlush: IsAsyncFlush; }`; + const oldInterface = `export interface FeatureOptions { enabled?: Enabled; }`; + const newInterface = `export interface FeatureOptions { enabled: Enabled; }`; const result = areInterfaceSignaturesCompatible(oldInterface, newInterface); expect(result).toBe(false); @@ -2577,8 +2577,8 @@ describe("areFunctionSignaturesCompatible", () => { test("should allow widening a field inside an options-object parameter", () => { // Real-world case: getSpanParentObject's options param widens its nested // `parent?` field from `string` to `string | PropagationContext`. - const oldFn = `export function getSpanParentObject(options?: AsyncFlushArg & OptionalStateArg & { parent?: string; }): Span`; - const newFn = `export function getSpanParentObject(options?: AsyncFlushArg & OptionalStateArg & { parent?: string | PropagationContext; }): Span`; + const oldFn = `export function getSpanParentObject(options?: ModeArg & OptionalStateArg & { parent?: string; }): Span`; + const newFn = `export function getSpanParentObject(options?: ModeArg & OptionalStateArg & { parent?: string | PropagationContext; }): Span`; expect(areFunctionSignaturesCompatible(oldFn, newFn)).toBe(true); }); From 51bef8cece4ee9f0ddfe1cfa626d778354ff2e93 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Sep 2026 13:58:50 +0200 Subject: [PATCH 08/19] ref!: Reduce API to a minimum that makes sense (#2429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) - Replaced the root `braintrust` export surface with an explicit allowlist, removing roughly 140 named exports—primarily internal helpers, implementation classes, schemas, constants, and low-level types. - Stopped named-exporting most SDK-owned request/result types. Retained types are primarily user extension points, including `StartSpanArgs`, `Span`, tracing context types, eval callbacks, reporters, template plugins, and durable-eval stores. - Removed package subpaths `braintrust/browser`, `braintrust/util`, and `braintrust/webpack-loader`; the util bundle is no longer built or published. - Kept the webpack loader private: `braintrust/next` now resolves its built loader directly instead of exposing it as a package entrypoint. - Removed public implementation/testing exports such as `_exportsForTestingOnly`, `_internalIso`, `_internalGetGlobalState`, `_internalSetInitialState`, `SpanImpl`, `NoopSpan`, `TestBackgroundLogger`, `ObjectFetcher`, and ID-generator classes. - Removed public runtime schemas and serialization internals, including prompt, stream-chunk, attachment-reference, logs-overflow, and propagation schemas/constants. - Removed lower-level project/building APIs such as `graph`, `Project`, `CodeFunction`, `CodePrompt`, `PromptBuilder`, `ScorerBuilder`, and `ToolBuilder`; the `projects` facade remains. - Removed miscellaneous convenience and implementation exports including `log`, `summarize`, `traceable`, `newId`, `permalink`, `deepCopyEvent`, template-registry internals, span fetchers, and logs-overflow helpers. - Removed deprecated APIs: - `BraintrustMiddleware` and `wrapAISDKModel` - `wrapMastraAgent` - `withExperiment` and `withDataset` - `toAIStreamResponse` - deprecated bundler aliases `esbuildPlugin`, `rollupPlugin`, `vitePlugin`, and `webpackPlugin` - deprecated `useDiagnosticChannelCompatShim` - `Experiment.close()`, `Dataset.close()`, and `Span.close()` - `EvalHooks.meta`, `EvalResultWithSummary.toString()`, and `Score.error` - Removed legacy aliases and call shapes: - `initExperiment` - string-first `init()` and `initDataset()` overloads - dataset `useOutput` mode and the `output` record alias - deprecated event `inputs` and `datasetRecordId` fields - `wrapOpenAIv4` - Changed `EvalResultWithSummary` from an exported runtime class to a plain `{ summary, results }` result shape. - Restricted public span parenting to opaque `PropagationContext`; serialized span-slug strings remain internal plumbing. The public type is still named `StartSpanArgs`. - Added `configureContextManager()` and `ContextManager.getCurrentSpanStore()` as the supported context integration mechanism, replacing global symbols and direct state mutation. - Moved instrumentation-only APIs off the root: - `configureInstrumentation`, `InstrumentationConfig`, and Eve/Flue instrumentation remain under `braintrust/instrumentation`. - `registerOtelFlush` now lives there as the narrow OTEL hook. - Simplified OTEL integration: - Uses `configureContextManager()` instead of internal globals. - No longer overrides ID generation or V4 span serialization. - Replaced `contextFromSpanExport(string)` with `contextFromSpan(span)`. - `parentFromHeaders()` now returns opaque W3C propagation context. - Environment and `.env.braintrust` handling is self-contained and Edge-safe. - Removed the OTEL-specific exported ID generator. - Migrated Temporal propagation from private serialized span headers to standard `traceparent`, `tracestate`, and `baggage`, covering client→workflow, child workflow, and same-/cross-worker activity parenting. - Reworked `@braintrust/browser` to install a browser `ContextManager` through the public configuration API instead of mutating core isomorph internals. - Updated OpenAI Agents and the Vercel adapter to avoid `braintrust/util` and the exported `BraintrustStreamChunk`; the adapter now derives its stream chunk type from `BraintrustStream`. - Canonicalized instrumentation config keys by removing typed aliases such as `vercel`, `google`, `cursor`, `bedrock`, and `awsBedrock`; environment spellings still map to canonical keys. - Updated smoke tests, integration tests, Deno/browser coverage, and e2e scenarios to use only the curated public surface; added explicit tests for the allowed root types and published package entrypoints. --- .changeset/reduce-api-to-a-minimum.md | 10 + e2e/package.json | 1 + .../vite.config.ts | 4 +- .../vite.config.ts | 4 +- .../vite.config.ts | 4 +- .../__snapshots__/request-flow.json | 200 +++++-- .../deno-browser/__snapshots__/span-tree.json | 86 +-- .../deno-browser/__snapshots__/span-tree.txt | 58 +- e2e/scenarios/deno-browser/runner.case.ts | 11 +- e2e/scenarios/deno-browser/scenario.test.ts | 14 +- e2e/scenarios/deno-node/runner.case.ts | 9 +- .../agent/instrumentation/braintrust.ts | 3 +- .../researcher/instrumentation/braintrust.ts | 3 +- e2e/scenarios/eve-instrumentation/scenario.ts | 2 +- .../flue-instrumentation/.flue/app.mjs | 3 +- .../flue-instrumentation/scenario.v2.ts | 8 +- .../otel-compat-mixed-tracing/scenario.ts | 6 +- .../test-framework-evals-jest/runner.case.cjs | 13 +- .../scenario.ts | 5 +- integrations/browser-js/src/browser-config.ts | 60 +-- integrations/langchain-js/src/test/utils.ts | 2 +- integrations/openai-agents-js/src/index.ts | 20 +- .../src/openai-agents-integration.test.ts | 6 +- .../openai-agents-js/src/test-logger.d.ts | 12 + integrations/openai-agents-js/tsconfig.json | 2 +- .../openai-agents-js/vitest.config.ts | 11 + integrations/otel-js/README.md | 15 +- integrations/otel-js/src/context.ts | 31 +- integrations/otel-js/src/exporter.test.ts | 2 +- integrations/otel-js/src/id-gen.test.ts | 7 +- integrations/otel-js/src/index.ts | 36 +- integrations/otel-js/src/otel-compat.test.ts | 86 ++- integrations/otel-js/src/otel.test.ts | 237 ++------- integrations/otel-js/src/otel.ts | 475 ++++------------- integrations/otel-js/tests/utils.ts | 13 + integrations/otel-js/tsconfig.json | 4 +- integrations/otel-js/tsconfig.test.json | 4 +- integrations/otel-js/vitest.config.ts | 2 + integrations/temporal-js/src/interceptors.ts | 123 +---- integrations/temporal-js/src/sinks.ts | 20 +- integrations/temporal-js/src/temporal.test.ts | 35 +- integrations/temporal-js/src/utils.ts | 44 +- .../temporal-js/src/workflow-interceptors.ts | 44 +- integrations/temporal-js/tsconfig.json | 4 +- integrations/temporal-js/tsup.config.ts | 1 - .../vercel-ai-sdk/src/adapter.test.ts | 9 +- integrations/vercel-ai-sdk/src/adapter.ts | 19 +- js/README.md | 4 +- js/package.json | 18 +- .../scenarios/browser-main-package/README.md | 30 +- .../src/browser-message-test.ts | 19 +- .../tests/browser-message.test.ts | 6 +- .../tests/node-esm-import.test.mjs | 4 +- .../cloudflare-vite-hono-vite-dev/README.md | 2 +- .../README.md | 2 +- .../playwright-browser/src/browser-tests.ts | 9 - js/smoke/shared/src/helpers/register.ts | 65 ++- js/smoke/shared/src/suites/basic-logging.ts | 7 + .../shared/src/suites/import-verification.ts | 133 +++-- .../auto-instrumentations/bundler/esbuild.ts | 7 - js/src/auto-instrumentations/bundler/next.ts | 16 +- .../auto-instrumentations/bundler/plugin.ts | 14 +- .../auto-instrumentations/bundler/rollup.ts | 7 - js/src/auto-instrumentations/bundler/vite.ts | 7 - .../bundler/webpack-loader.ts | 19 +- .../auto-instrumentations/bundler/webpack.ts | 7 - js/src/auto-instrumentations/configs/all.ts | 8 +- js/src/browser/index.ts | 1 - js/src/edge-light/index.ts | 1 - js/src/edge-runtime-bootstrap.test.ts | 18 +- js/src/exports.ts | 233 +------- js/src/framework.test.ts | 6 +- js/src/framework.ts | 62 +-- js/src/framework2.ts | 16 +- js/src/functions/invoke.ts | 8 +- js/src/functions/stream.ts | 69 ++- js/src/graph-framework.ts | 499 ------------------ js/src/id-gen.ts | 9 +- js/src/instrumentation/config.ts | 22 +- .../instrumentation/core/channel-tracing.ts | 16 +- js/src/instrumentation/index.test.ts | 1 + js/src/instrumentation/index.ts | 3 + .../instrumentation-consumers.test.ts | 13 +- .../instrumentation-consumers.ts | 14 +- .../claude-agent-sdk-instrumentation.test.ts | 14 +- .../claude-agent-sdk-instrumentation.ts | 2 +- .../claude-agent-sdk-local-tool-spans.ts | 2 +- .../cloudflare-ai-chat-consumer.test.ts | 22 +- .../providers/cloudflare-ai-chat-consumer.ts | 11 +- .../cloudflare-think-instrumentation.ts | 16 +- .../cursor-sdk-instrumentation.test.ts | 1 + .../providers/cursor-sdk-instrumentation.ts | 2 +- .../providers/eve-instrumentation.test.ts | 6 +- .../providers/flue-instrumentation.test.ts | 7 +- .../providers/flue-instrumentation.ts | 7 +- .../providers/genkit-instrumentation.ts | 9 +- .../github-copilot-instrumentation.ts | 2 +- .../google-adk-instrumentation.test.ts | 11 +- .../providers/google-adk-instrumentation.ts | 9 +- .../providers/google-genai-instrumentation.ts | 9 +- .../openai-codex-instrumentation.test.ts | 1 + .../providers/openai-codex-instrumentation.ts | 8 +- .../pi-coding-agent-instrumentation.test.ts | 1 + .../pi-coding-agent-instrumentation.ts | 2 +- js/src/instrumentation/registry.ts | 2 +- js/src/logger-misc.test.ts | 6 +- js/src/logger.test.ts | 15 +- js/src/logger.ts | 399 +++----------- js/src/node/index.ts | 1 - js/src/openai-utils.ts | 6 +- js/src/package-exports.test.ts | 37 ++ js/src/prompt-schemas.ts | 46 -- js/src/propagation.test.ts | 17 +- js/src/propagation.ts | 2 +- js/src/public-types.test.ts | 135 ++--- js/src/sandbox.ts | 6 +- js/src/template/registry.ts | 2 +- js/src/trace.ts | 2 +- js/src/workerd/index.ts | 1 - .../ai-sdk/deprecated/BraintrustMiddleware.ts | 493 ----------------- .../ai-sdk/deprecated/wrapAISDKModel.ts | 362 ------------- .../wrappers/ai-sdk/harness-agent-context.ts | 7 +- js/src/wrappers/ai-sdk/index.ts | 4 - js/src/wrappers/exports.test.ts | 38 -- js/src/wrappers/langchain/callback-handler.ts | 5 +- js/src/wrappers/mastra.ts | 46 +- js/src/wrappers/node-test/suite.ts | 5 +- js/src/wrappers/node-test/test-helpers.ts | 14 +- js/src/wrappers/oai.ts | 9 +- js/src/wrappers/vitest-evals/reporter.test.ts | 2 +- js/src/wrappers/vitest-evals/reporter.ts | 9 +- js/src/wrappers/vitest/context-manager.ts | 4 +- .../vitest/vitest-wrapper-example.test.ts | 2 +- .../vitest/vitest-wrapper-span.test.ts | 2 +- js/src/wrappers/vitest/vitest-wrapper.test.ts | 27 +- js/src/wrappers/vitest/wrapper.ts | 7 +- .../api-compatibility.test.ts | 35 +- .../error-handling.test.ts | 28 +- .../event-content.test.ts | 80 +-- .../function-behavior.test.ts | 36 +- .../multiple-instrumentations.test.ts | 24 +- .../auto-instrumentations/next-config.test.ts | 10 +- .../runtime-execution.test.ts | 36 +- .../streaming-and-responses.test.ts | 24 +- .../transformation.test.ts | 10 +- js/tsup.config.ts | 31 +- js/util/object.ts | 3 +- js/util/score.ts | 5 - pnpm-lock.yaml | 3 + 149 files changed, 1406 insertions(+), 3917 deletions(-) create mode 100644 .changeset/reduce-api-to-a-minimum.md create mode 100644 integrations/openai-agents-js/src/test-logger.d.ts delete mode 100644 js/src/graph-framework.ts create mode 100644 js/src/package-exports.test.ts delete mode 100644 js/src/wrappers/ai-sdk/deprecated/BraintrustMiddleware.ts delete mode 100644 js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts delete mode 100644 js/src/wrappers/exports.test.ts diff --git a/.changeset/reduce-api-to-a-minimum.md b/.changeset/reduce-api-to-a-minimum.md new file mode 100644 index 000000000..958c893c9 --- /dev/null +++ b/.changeset/reduce-api-to-a-minimum.md @@ -0,0 +1,10 @@ +--- +"braintrust": major +"@braintrust/otel": major +"@braintrust/vercel-ai-sdk": major +"@braintrust/browser": patch +"@braintrust/openai-agents": patch +"@braintrust/temporal": patch +--- + +ref!: Reduce API to a minimum that makes sense diff --git a/e2e/package.json b/e2e/package.json index 8de220b7e..414b75247 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -8,6 +8,7 @@ "test:e2e:record": "node ./scripts/run-record-tests.mjs" }, "devDependencies": { + "@braintrust/browser": "workspace:^", "@braintrust/langchain-js": "workspace:^", "@braintrust/otel": "workspace:^", "@braintrust/seinfeld": "workspace:^", diff --git a/e2e/scenarios/cloudflare-agents-instrumentation/vite.config.ts b/e2e/scenarios/cloudflare-agents-instrumentation/vite.config.ts index ca68896e5..0e75a3319 100644 --- a/e2e/scenarios/cloudflare-agents-instrumentation/vite.config.ts +++ b/e2e/scenarios/cloudflare-agents-instrumentation/vite.config.ts @@ -7,9 +7,7 @@ const agentsPackageName = const instrumentationMode = process.env.CLOUDFLARE_AGENTS_INSTRUMENTATION_MODE ?? "auto"; const braintrustPlugins = - instrumentationMode === "auto" - ? braintrustVitePlugin({ useDiagnosticChannelCompatShim: true }) - : []; + instrumentationMode === "auto" ? braintrustVitePlugin({ browser: true }) : []; export default defineConfig({ define: { diff --git a/e2e/scenarios/cloudflare-ai-chat-instrumentation/vite.config.ts b/e2e/scenarios/cloudflare-ai-chat-instrumentation/vite.config.ts index 3952e5fd6..1c131a62d 100644 --- a/e2e/scenarios/cloudflare-ai-chat-instrumentation/vite.config.ts +++ b/e2e/scenarios/cloudflare-ai-chat-instrumentation/vite.config.ts @@ -25,9 +25,7 @@ export default defineConfig({ __OPENAI_BASE_URL__: JSON.stringify(process.env.OPENAI_BASE_URL), }, plugins: [ - ...(mode === "auto" - ? [braintrustVitePlugin({ useDiagnosticChannelCompatShim: true })] - : []), + ...(mode === "auto" ? [braintrustVitePlugin({ browser: true })] : []), cloudflare({ configPath: "./wrangler.toml", inspectorPort: 0 }), ], resolve: { diff --git a/e2e/scenarios/cloudflare-think-instrumentation/vite.config.ts b/e2e/scenarios/cloudflare-think-instrumentation/vite.config.ts index 9a28d8c50..d068998ef 100644 --- a/e2e/scenarios/cloudflare-think-instrumentation/vite.config.ts +++ b/e2e/scenarios/cloudflare-think-instrumentation/vite.config.ts @@ -13,9 +13,7 @@ export default defineConfig({ ), }, plugins: [ - ...(manual - ? [] - : [braintrustVitePlugin({ useDiagnosticChannelCompatShim: true })]), + ...(manual ? [] : [braintrustVitePlugin({ browser: true })]), cloudflare({ inspectorPort: 0 }), ], resolve: { diff --git a/e2e/scenarios/deno-browser/__snapshots__/request-flow.json b/e2e/scenarios/deno-browser/__snapshots__/request-flow.json index f8eea5a3f..ca74c4553 100644 --- a/e2e/scenarios/deno-browser/__snapshots__/request-flow.json +++ b/e2e/scenarios/deno-browser/__snapshots__/request-flow.json @@ -27,7 +27,11 @@ "api_version": 2, "rows": [ { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "expected": "Paris", "id": "", @@ -54,7 +58,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -89,7 +97,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -120,7 +132,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -142,18 +158,24 @@ "phase": "child" }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 3, - "name": "deno browser child span", - "type": "task" + "name": "deno browser child span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "async innerWrapped", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-parent", @@ -165,18 +187,22 @@ "start": 0 }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 4, "name": "deno browser nested parent span", "type": "task" }, - "span_id": "" + "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-child", @@ -188,18 +214,24 @@ "start": 0 }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 5, - "name": "deno browser nested child span", - "type": "task" + "name": "deno browser nested child span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-grandchild", @@ -210,19 +242,28 @@ "end": 0, "start": 0 }, + "output": { + "depth": 3 + }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 6, - "name": "deno browser nested grandchild span", - "type": "task" + "name": "deno browser nested grandchild span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "async innerWrapped", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "current-span", @@ -233,14 +274,17 @@ "end": 0, "start": 0 }, + "output": { + "observedSpanId": "" + }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 7, "name": "deno browser current span", "type": "task" }, - "span_id": "" + "span_id": "" } ] }, @@ -251,7 +295,11 @@ "api_version": 2, "rows": [ { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "expected": "Paris", "id": "", @@ -278,7 +326,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -313,7 +365,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "innerWrapped", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -344,7 +400,11 @@ "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", "id": "", "input": { @@ -366,18 +426,24 @@ "phase": "child" }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 3, - "name": "deno browser child span", - "type": "task" + "name": "deno browser child span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "async innerWrapped", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-parent", @@ -389,18 +455,22 @@ "start": 0 }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 4, "name": "deno browser nested parent span", "type": "task" }, - "span_id": "" + "span_id": "" }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-child", @@ -412,18 +482,24 @@ "start": 0 }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 5, - "name": "deno browser nested child span", - "type": "task" + "name": "deno browser nested child span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "/e2e/scenarios/deno-browser/runner.case.ts", + "caller_functionname": "braintrust.traced.name", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "nested-grandchild", @@ -434,19 +510,28 @@ "end": 0, "start": 0 }, + "output": { + "depth": 3 + }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 6, - "name": "deno browser nested grandchild span", - "type": "task" + "name": "deno browser nested grandchild span" }, - "span_id": "" + "span_id": "", + "span_parents": [ + "" + ] }, { - "context": {}, + "context": { + "caller_filename": "ext:cli/40_test.js", + "caller_functionname": "async innerWrapped", + "caller_lineno": 0 + }, "created": "", - "id": "", + "id": "", "log_id": "g", "metadata": { "case": "current-span", @@ -457,14 +542,17 @@ "end": 0, "start": 0 }, + "output": { + "observedSpanId": "" + }, "project_id": "", - "root_span_id": "", + "root_span_id": "", "span_attributes": { "exec_counter": 7, "name": "deno browser current span", "type": "task" }, - "span_id": "" + "span_id": "" } ] } diff --git a/e2e/scenarios/deno-browser/__snapshots__/span-tree.json b/e2e/scenarios/deno-browser/__snapshots__/span-tree.json index 9461d0edb..d5ffa8e80 100644 --- a/e2e/scenarios/deno-browser/__snapshots__/span-tree.json +++ b/e2e/scenarios/deno-browser/__snapshots__/span-tree.json @@ -39,7 +39,25 @@ { "name": "deno browser parent span", "type": "task", - "children": [], + "children": [ + { + "name": "deno browser child span", + "children": [], + "input": { + "step": "child", + "testRunId": "" + }, + "output": { + "ok": true, + "phase": "child" + }, + "metadata": { + "case": "child-span", + "scenario": "deno-browser", + "testRunId": "" + } + } + ], "input": { "phase": "parent", "testRunId": "" @@ -54,58 +72,46 @@ "testRunId": "" } }, - { - "name": "deno browser child span", - "type": "task", - "children": [], - "input": { - "step": "child", - "testRunId": "" - }, - "output": { - "ok": true, - "phase": "child" - }, - "metadata": { - "case": "child-span", - "scenario": "deno-browser", - "testRunId": "" - } - }, { "name": "deno browser nested parent span", "type": "task", - "children": [], + "children": [ + { + "name": "deno browser nested child span", + "children": [ + { + "name": "deno browser nested grandchild span", + "children": [], + "output": { + "depth": 3 + }, + "metadata": { + "case": "nested-grandchild", + "scenario": "deno-browser", + "testRunId": "" + } + } + ], + "metadata": { + "case": "nested-child", + "scenario": "deno-browser", + "testRunId": "" + } + } + ], "metadata": { "case": "nested-parent", "scenario": "deno-browser", "testRunId": "" } }, - { - "name": "deno browser nested child span", - "type": "task", - "children": [], - "metadata": { - "case": "nested-child", - "scenario": "deno-browser", - "testRunId": "" - } - }, - { - "name": "deno browser nested grandchild span", - "type": "task", - "children": [], - "metadata": { - "case": "nested-grandchild", - "scenario": "deno-browser", - "testRunId": "" - } - }, { "name": "deno browser current span", "type": "task", "children": [], + "output": { + "observedSpanId": "" + }, "metadata": { "case": "current-span", "scenario": "deno-browser", diff --git a/e2e/scenarios/deno-browser/__snapshots__/span-tree.txt b/e2e/scenarios/deno-browser/__snapshots__/span-tree.txt index 9af1c12e1..3f459283e 100644 --- a/e2e/scenarios/deno-browser/__snapshots__/span-tree.txt +++ b/e2e/scenarios/deno-browser/__snapshots__/span-tree.txt @@ -41,39 +41,45 @@ span_tree: │ "scenario": "deno-browser", │ "testRunId": "" │ } -├── deno browser child span [task] -│ input: { -│ "step": "child", -│ "testRunId": "" -│ } -│ output: { -│ "ok": true, -│ "phase": "child" -│ } -│ metadata: { -│ "case": "child-span", -│ "scenario": "deno-browser", -│ "testRunId": "" -│ } +│ └── deno browser child span +│ input: { +│ "step": "child", +│ "testRunId": "" +│ } +│ output: { +│ "ok": true, +│ "phase": "child" +│ } +│ metadata: { +│ "case": "child-span", +│ "scenario": "deno-browser", +│ "testRunId": "" +│ } ├── deno browser nested parent span [task] │ metadata: { │ "case": "nested-parent", │ "scenario": "deno-browser", │ "testRunId": "" │ } -├── deno browser nested child span [task] -│ metadata: { -│ "case": "nested-child", -│ "scenario": "deno-browser", -│ "testRunId": "" -│ } -├── deno browser nested grandchild span [task] -│ metadata: { -│ "case": "nested-grandchild", -│ "scenario": "deno-browser", -│ "testRunId": "" -│ } +│ └── deno browser nested child span +│ metadata: { +│ "case": "nested-child", +│ "scenario": "deno-browser", +│ "testRunId": "" +│ } +│ └── deno browser nested grandchild span +│ output: { +│ "depth": 3 +│ } +│ metadata: { +│ "case": "nested-grandchild", +│ "scenario": "deno-browser", +│ "testRunId": "" +│ } └── deno browser current span [task] + output: { + "observedSpanId": "" + } metadata: { "case": "current-span", "scenario": "deno-browser", diff --git a/e2e/scenarios/deno-browser/runner.case.ts b/e2e/scenarios/deno-browser/runner.case.ts index 88197b73b..823b53dc6 100644 --- a/e2e/scenarios/deno-browser/runner.case.ts +++ b/e2e/scenarios/deno-browser/runner.case.ts @@ -1,6 +1,5 @@ -import * as braintrust from "braintrust/browser"; +import * as braintrust from "@braintrust/browser"; import { - expectBuildType, expectEvalWorks, expectMustacheTemplate, expectNamedExports, @@ -27,10 +26,8 @@ Deno.test("deno-browser exposes the expected browser runtime surface", () => { "initLogger", "currentSpan", "startSpan", - "log", "flush", "initDataset", - "initExperiment", "loadPrompt", "Prompt", "Eval", @@ -40,14 +37,10 @@ Deno.test("deno-browser exposes the expected browser runtime surface", () => { "Attachment", "invoke", "initFunction", - "Project", - "PromptBuilder", - "IDGenerator", + "projects", "BraintrustState", "login", - "_exportsForTestingOnly", ]); - expectBuildType(braintrust, "browser"); expectMustacheTemplate(braintrust); expectNunjucksTemplateUnavailable(braintrust); }); diff --git a/e2e/scenarios/deno-browser/scenario.test.ts b/e2e/scenarios/deno-browser/scenario.test.ts index 46fe46f7d..eb066f2d1 100644 --- a/e2e/scenarios/deno-browser/scenario.test.ts +++ b/e2e/scenarios/deno-browser/scenario.test.ts @@ -128,11 +128,15 @@ test( phase: "child", ok: true, }); - expect(childSpan?.span.parentIds).toEqual([]); - expect(nestedChild?.span.parentIds).toEqual([]); - expect(nestedGrandchild?.span.parentIds).toEqual([]); - expect(nestedGrandchild?.output).toBeUndefined(); - expect(currentSpan?.output).toBeUndefined(); + expect(childSpan?.span.parentIds).toEqual([parentSpan?.span.id]); + expect(nestedChild?.span.parentIds).toEqual([nestedParent?.span.id]); + expect(nestedGrandchild?.span.parentIds).toEqual([ + nestedChild?.span.id, + ]); + expect(nestedGrandchild?.output).toEqual({ depth: 3 }); + expect(currentSpan?.output).toMatchObject({ + observedSpanId: currentSpan?.span.id, + }); const requests = requestsAfter( cursor, diff --git a/e2e/scenarios/deno-node/runner.case.ts b/e2e/scenarios/deno-node/runner.case.ts index b8f88b2db..2f21d1d91 100644 --- a/e2e/scenarios/deno-node/runner.case.ts +++ b/e2e/scenarios/deno-node/runner.case.ts @@ -1,6 +1,5 @@ import * as braintrust from "braintrust"; import { - expectBuildType, expectEvalWorks, expectMustacheTemplate, expectNamedExports, @@ -27,10 +26,8 @@ Deno.test("deno-node exposes the expected node runtime surface", () => { "initLogger", "currentSpan", "startSpan", - "log", "flush", "initDataset", - "initExperiment", "loadPrompt", "Prompt", "Eval", @@ -40,14 +37,10 @@ Deno.test("deno-node exposes the expected node runtime surface", () => { "Attachment", "invoke", "initFunction", - "Project", - "PromptBuilder", - "IDGenerator", + "projects", "BraintrustState", "login", - "_exportsForTestingOnly", ]); - expectBuildType(braintrust, "node"); expectMustacheTemplate(braintrust); expectNunjucksTemplateUnavailable(braintrust); }); diff --git a/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts index 9ba5484f2..b16977e74 100644 --- a/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts +++ b/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts @@ -1,4 +1,5 @@ -import { braintrustEveInstrumentation, initLogger } from "braintrust"; +import { initLogger } from "braintrust"; +import { braintrustEveInstrumentation } from "braintrust/instrumentation"; import { defineInstrumentation } from "eve/instrumentation"; export default defineInstrumentation( diff --git a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts index 9ba5484f2..b16977e74 100644 --- a/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts +++ b/e2e/scenarios/eve-instrumentation/agent/subagents/researcher/instrumentation/braintrust.ts @@ -1,4 +1,5 @@ -import { braintrustEveInstrumentation, initLogger } from "braintrust"; +import { initLogger } from "braintrust"; +import { braintrustEveInstrumentation } from "braintrust/instrumentation"; import { defineInstrumentation } from "eve/instrumentation"; export default defineInstrumentation( diff --git a/e2e/scenarios/eve-instrumentation/scenario.ts b/e2e/scenarios/eve-instrumentation/scenario.ts index ca1c92027..446677607 100644 --- a/e2e/scenarios/eve-instrumentation/scenario.ts +++ b/e2e/scenarios/eve-instrumentation/scenario.ts @@ -80,7 +80,7 @@ async function main() { ); // Eve emits session.waiting just before its durable session snapshot is // visible to the continuation route. - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 3000)); const followUp = await fetch( `${baseUrl}/eve/v1/session/${body.sessionId}`, { diff --git a/e2e/scenarios/flue-instrumentation/.flue/app.mjs b/e2e/scenarios/flue-instrumentation/.flue/app.mjs index 5d3ce1360..7e22bc269 100644 --- a/e2e/scenarios/flue-instrumentation/.flue/app.mjs +++ b/e2e/scenarios/flue-instrumentation/.flue/app.mjs @@ -1,4 +1,5 @@ -import { braintrustFlueInstrumentation, flush, initLogger } from "braintrust"; +import { flush, initLogger } from "braintrust"; +import { braintrustFlueInstrumentation } from "braintrust/instrumentation"; import { Hono } from "hono"; const runtimePackageName = diff --git a/e2e/scenarios/flue-instrumentation/scenario.v2.ts b/e2e/scenarios/flue-instrumentation/scenario.v2.ts index 567d47321..8d9c41a06 100644 --- a/e2e/scenarios/flue-instrumentation/scenario.v2.ts +++ b/e2e/scenarios/flue-instrumentation/scenario.v2.ts @@ -1,10 +1,6 @@ import { openaiProvider } from "pi-ai-v2/providers/openai"; -import { - braintrustFlueInstrumentation, - flush, - initLogger, - traced, -} from "braintrust"; +import { flush, initLogger, traced } from "braintrust"; +import { braintrustFlueInstrumentation } from "braintrust/instrumentation"; import * as v from "valibot"; const SCENARIO_NAME = "flue-instrumentation"; diff --git a/e2e/scenarios/otel-compat-mixed-tracing/scenario.ts b/e2e/scenarios/otel-compat-mixed-tracing/scenario.ts index f3a2d4bb1..f74828b5e 100644 --- a/e2e/scenarios/otel-compat-mixed-tracing/scenario.ts +++ b/e2e/scenarios/otel-compat-mixed-tracing/scenario.ts @@ -1,7 +1,7 @@ import { context as otelContext, trace } from "@opentelemetry/api"; import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks"; import { BraintrustSpanProcessor, setupOtelCompat } from "@braintrust/otel"; -import { getContextManager, initLogger } from "braintrust"; +import { initLogger, withCurrent } from "braintrust"; import { createTracerProvider, getTestRunId, @@ -39,9 +39,7 @@ async function main() { }, }, }); - const contextManagerFacade = getContextManager(); - - await contextManagerFacade.runInContext(btRoot, async () => { + await withCurrent(btRoot, async () => { await tracer.startActiveSpan("otel-middle", async (otelSpan) => { const btChild = logger.startSpan({ name: "bt-child-under-otel", diff --git a/e2e/scenarios/test-framework-evals-jest/runner.case.cjs b/e2e/scenarios/test-framework-evals-jest/runner.case.cjs index cffa7756d..b2886017b 100644 --- a/e2e/scenarios/test-framework-evals-jest/runner.case.cjs +++ b/e2e/scenarios/test-framework-evals-jest/runner.case.cjs @@ -2,7 +2,6 @@ const { Eval, JSONAttachment, Prompt, - _exportsForTestingOnly, currentSpan, initLogger, startSpan, @@ -40,10 +39,8 @@ test("jest exposes the core braintrust runtime surface in CommonJS", () => { "initLogger", "currentSpan", "startSpan", - "log", "flush", "initDataset", - "initExperiment", "loadPrompt", "Prompt", "Eval", @@ -53,20 +50,12 @@ test("jest exposes the core braintrust runtime surface in CommonJS", () => { "Attachment", "invoke", "initFunction", - "Project", - "PromptBuilder", - "IDGenerator", + "projects", "BraintrustState", "login", - "_exportsForTestingOnly", ]); }); -test("jest resolves the node build in CommonJS mode", () => { - expect(_exportsForTestingOnly).toBeDefined(); - expect(_exportsForTestingOnly.isomorph.buildType).toBe("node"); -}); - test("jest renders mustache prompts and fails clearly for nunjucks without the extra package", () => { const mustachePrompt = new Prompt( { diff --git a/e2e/scenarios/trace-context-and-continuation/scenario.ts b/e2e/scenarios/trace-context-and-continuation/scenario.ts index c0bb88ec3..bcf1371ba 100644 --- a/e2e/scenarios/trace-context-and-continuation/scenario.ts +++ b/e2e/scenarios/trace-context-and-continuation/scenario.ts @@ -1,5 +1,6 @@ import { flush, + extractTraceContextFromHeaders, initLogger, startSpan, traced, @@ -28,7 +29,7 @@ async function main() { }, }, }); - const exportedRoot = await rootSpan.export(); + const propagatedRoot = extractTraceContextFromHeaders(rootSpan.inject()); await withCurrent(rootSpan, async () => { const currentChild = startSpan({ @@ -50,7 +51,7 @@ async function main() { rootSpan.end(); - await withParent(exportedRoot, async () => { + await withParent(propagatedRoot, async () => { await traced( (span) => { span.log({ diff --git a/integrations/browser-js/src/browser-config.ts b/integrations/browser-js/src/browser-config.ts index ddf239c33..ae9c788a6 100644 --- a/integrations/browser-js/src/browser-config.ts +++ b/integrations/browser-js/src/browser-config.ts @@ -1,41 +1,35 @@ -import { _internalIso as iso, _internalSetInitialState } from "braintrust"; +import { + configureContextManager, + ContextManager, + type ContextParentSpanIds, + type CurrentSpanStore, + type Span, +} from "braintrust"; import { AsyncLocalStorage as BrowserAsyncLocalStorage } from "als-browser"; -export function configureBrowser() { - // Set build type indicator - iso.buildType = "browser-js" as "browser"; +class BrowserContextManager extends ContextManager { + private readonly currentSpan = new BrowserAsyncLocalStorage(); - iso.newAsyncLocalStorage = () => new BrowserAsyncLocalStorage(); + getParentSpanIds(): ContextParentSpanIds | undefined { + const span = this.currentSpan.getStore(); + return span + ? { rootSpanId: span.rootSpanId, spanParents: [span.spanId] } + : undefined; + } - iso.getEnv = (name: string) => { - if (typeof process === "undefined" || typeof process.env === "undefined") { - return undefined; - } - return process.env[name]; - }; + runInContext(span: Span, callback: () => R): R { + return this.currentSpan.run(span, callback); + } - // noop implementations for git config - iso.getRepoInfo = async () => ({ - commit: null, - branch: null, - tag: null, - dirty: false, - }); - iso.getCallerLocation = () => undefined; + getCurrentSpan(): Span | undefined { + return this.currentSpan.getStore(); + } - // Implement browser-compatible hash function using a simple hash algorithm - iso.hash = (data: string): string => { - // Simple hash function for browser compatibility - let hash = 0; - for (let i = 0; i < data.length; i++) { - const char = data.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32bit integer - } - // Convert to hex string - const hashHex = (hash >>> 0).toString(16).padStart(8, "0"); - return hashHex.repeat(8).substring(0, 64); // Make it look like a SHA-256 hash length - }; + getCurrentSpanStore(): CurrentSpanStore { + return this.currentSpan; + } +} - _internalSetInitialState(); +export function configureBrowser(): void { + configureContextManager(BrowserContextManager); } diff --git a/integrations/langchain-js/src/test/utils.ts b/integrations/langchain-js/src/test/utils.ts index 823dc5b71..52761197c 100644 --- a/integrations/langchain-js/src/test/utils.ts +++ b/integrations/langchain-js/src/test/utils.ts @@ -1,4 +1,4 @@ -import { mergeDicts } from "braintrust/util"; +import { mergeDicts } from "../../../../js/util/object_util"; import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; import { LogsRequest } from "./types"; diff --git a/integrations/openai-agents-js/src/index.ts b/integrations/openai-agents-js/src/index.ts index beafc3fc0..233bfa9bb 100644 --- a/integrations/openai-agents-js/src/index.ts +++ b/integrations/openai-agents-js/src/index.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { SpanTypeAttribute } from "braintrust/util"; import { Span as BraintrustSpan, startSpan, @@ -7,6 +6,7 @@ import { currentSpan, NOOP_SPAN, Attachment, + type StartSpanArgs, } from "braintrust"; import { SpanType, @@ -29,7 +29,9 @@ import { isSpeechGroupSpanData, } from "./types"; -function spanTypeFromAgents(span: AgentsSpan): SpanTypeAttribute { +function spanTypeFromAgents( + span: AgentsSpan, +): NonNullable { const spanType = span.spanData.type; if ( @@ -38,7 +40,7 @@ function spanTypeFromAgents(span: AgentsSpan): SpanTypeAttribute { spanType === SpanType.CUSTOM || spanType === SpanType.SPEECH_GROUP ) { - return SpanTypeAttribute.TASK; + return "task"; } if ( @@ -46,7 +48,7 @@ function spanTypeFromAgents(span: AgentsSpan): SpanTypeAttribute { spanType === SpanType.GUARDRAIL || spanType === SpanType.MCP_TOOLS ) { - return SpanTypeAttribute.TOOL; + return "tool"; } if ( @@ -55,10 +57,10 @@ function spanTypeFromAgents(span: AgentsSpan): SpanTypeAttribute { spanType === SpanType.TRANSCRIPTION || spanType === SpanType.SPEECH ) { - return SpanTypeAttribute.LLM; + return "llm"; } - return SpanTypeAttribute.TASK; + return "task"; } function spanNameFromAgents(span: AgentsSpan): string { @@ -282,18 +284,18 @@ export class OpenAIAgentsTraceProcessor { // Create as child of current span span = current.startSpan({ name: trace.name, - type: SpanTypeAttribute.TASK, + type: "task", }); } else { // No parent span available, create as root span = this.logger ? this.logger.startSpan({ name: trace.name, - type: SpanTypeAttribute.TASK, + type: "task", }) : startSpan({ name: trace.name, - type: SpanTypeAttribute.TASK, + type: "task", }); } diff --git a/integrations/openai-agents-js/src/openai-agents-integration.test.ts b/integrations/openai-agents-js/src/openai-agents-integration.test.ts index 71a6e5fd5..e6f59c0cd 100644 --- a/integrations/openai-agents-js/src/openai-agents-integration.test.ts +++ b/integrations/openai-agents-js/src/openai-agents-integration.test.ts @@ -11,14 +11,16 @@ import { z } from "zod/v3"; // Import necessary types and functions from braintrust import { - _exportsForTestingOnly, initLogger, Logger, - TestBackgroundLogger, Span as BraintrustSpan, wrapTraced, currentSpan, } from "braintrust"; +import { + _exportsForTestingOnly, + type TestBackgroundLogger, +} from "@braintrust-test/logger"; // Test helper functions for backward compatibility function getSpansMap( diff --git a/integrations/openai-agents-js/src/test-logger.d.ts b/integrations/openai-agents-js/src/test-logger.d.ts new file mode 100644 index 000000000..1cc19256d --- /dev/null +++ b/integrations/openai-agents-js/src/test-logger.d.ts @@ -0,0 +1,12 @@ +declare module "@braintrust-test/logger" { + export interface TestBackgroundLogger { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + drain(): Promise; + } + + export const _exportsForTestingOnly: { + simulateLoginForTests(): Promise; + useTestBackgroundLogger(): TestBackgroundLogger; + clearTestBackgroundLogger(): void; + }; +} diff --git a/integrations/openai-agents-js/tsconfig.json b/integrations/openai-agents-js/tsconfig.json index ea6ad1861..5fd07c79d 100644 --- a/integrations/openai-agents-js/tsconfig.json +++ b/integrations/openai-agents-js/tsconfig.json @@ -10,6 +10,6 @@ "esModuleInterop": true, "skipLibCheck": true }, - "include": ["."], + "include": ["src"], "exclude": ["node_modules/**", "**/dist/**"] } diff --git a/integrations/openai-agents-js/vitest.config.ts b/integrations/openai-agents-js/vitest.config.ts index 9b3c6bb43..1c14e5ef6 100644 --- a/integrations/openai-agents-js/vitest.config.ts +++ b/integrations/openai-agents-js/vitest.config.ts @@ -1,6 +1,17 @@ import { defineConfig } from "vitest/config"; +import { fileURLToPath } from "node:url"; export default defineConfig({ + resolve: { + alias: { + "@braintrust-test/logger": fileURLToPath( + new URL("../../js/src/logger.ts", import.meta.url), + ), + braintrust: fileURLToPath( + new URL("../../js/src/node/index.ts", import.meta.url), + ), + }, + }, test: { reporters: ["default"], include: ["tests/**/*.test.ts", "src/**/*.test.ts"], diff --git a/integrations/otel-js/README.md b/integrations/otel-js/README.md index f5adf45c2..e7afa2da5 100644 --- a/integrations/otel-js/README.md +++ b/integrations/otel-js/README.md @@ -118,28 +118,23 @@ const logger = initLogger({ projectName: "my_project" }); **What it does**: - Sets Braintrust to use OpenTelemetry's context manager for parent-child span relationships -- Configures Braintrust to use OpenTelemetry-compatible span and trace IDs - Enables seamless mixing of Braintrust and OpenTelemetry spans in the same trace ## Distributed Tracing Helpers These utilities enable distributed tracing across services that use different tracing systems (Braintrust and OpenTelemetry). -### `contextFromSpanExport(exportStr: string)` +### `contextFromSpan(span: Span)` -Creates an OpenTelemetry context from a Braintrust span export string. +Creates an OpenTelemetry context from a Braintrust span using W3C Trace Context. -**When to use it**: When Service A uses Braintrust and sends a span export to Service B that uses OpenTelemetry. This allows Service B to create spans as children of Service A's span. +**When to use it**: When Braintrust and OpenTelemetry code run in the same process and an OpenTelemetry operation should be a child of a Braintrust span. Across services, propagate the headers returned by `span.inject()` instead. ```typescript -import { contextFromSpanExport } from "@braintrust/otel"; +import { contextFromSpan } from "@braintrust/otel"; import { context } from "@opentelemetry/api"; -// Service A (Braintrust) exports span -const exportedSpan = await spanA.export(); - -// Service B (OpenTelemetry) imports context -const ctx = contextFromSpanExport(exportedSpan); +const ctx = contextFromSpan(spanA); await context.with(ctx, async () => { // OpenTelemetry spans created here will be children of Service A's span await tracer.startActiveSpan("service_b_operation", async (span) => { diff --git a/integrations/otel-js/src/context.ts b/integrations/otel-js/src/context.ts index 6db697c67..b61416e41 100644 --- a/integrations/otel-js/src/context.ts +++ b/integrations/otel-js/src/context.ts @@ -1,11 +1,10 @@ import { ContextManager, - BRAINTRUST_CURRENT_SPAN_STORE, - _internalIso as iso, type ContextParentSpanIds, type CurrentSpanStore, type Span, } from "braintrust"; +import { AsyncLocalStorage } from "node:async_hooks"; import { trace as otelTrace, context as otelContext } from "@opentelemetry/api"; import { getOtelParentFromSpan } from "./otel"; @@ -91,29 +90,11 @@ export class OtelContextManager extends ContextManager { return (otelContext as any)._getContextManager?.()._asyncLocalStorage; } - constructor() { - super(); - // Expose whichever ALS is in use via BRAINTRUST_CURRENT_SPAN_STORE so that - // TracingChannel's bindStore can propagate span context. We prefer OTEL's own - // ALS (AsyncLocalStorageContextManager._asyncLocalStorage) so that spans - // stored by runStores are visible to OTEL's context APIs. If the active OTEL - // context manager doesn't expose an ALS (e.g. AsyncHooksContextManager), we - // fall back to our own IsoAsyncLocalStorage and behave like the default - // BraintrustContextManager for TracingChannel binding. - // - // A lazy getter is required because the global OTEL context manager may not be - // registered until after this instance is constructed. - const self = this; - Object.defineProperty(this, BRAINTRUST_CURRENT_SPAN_STORE, { - get(): CurrentSpanStore { - const otelAls = self._getOtelAls(); - if (otelAls) return otelAls; - if (!self._ownAls) self._ownAls = iso.newAsyncLocalStorage(); - return self._ownAls; - }, - configurable: true, - enumerable: false, - }); + getCurrentSpanStore(): CurrentSpanStore { + const otelAls = this._getOtelAls(); + if (otelAls) return otelAls; + if (!this._ownAls) this._ownAls = new AsyncLocalStorage(); + return this._ownAls; } wrapSpanForStore(span: Span): unknown { diff --git a/integrations/otel-js/src/exporter.test.ts b/integrations/otel-js/src/exporter.test.ts index 94168963a..b0391222d 100644 --- a/integrations/otel-js/src/exporter.test.ts +++ b/integrations/otel-js/src/exporter.test.ts @@ -22,7 +22,7 @@ import { } from "@opentelemetry/sdk-trace-base"; import { BraintrustSpanProcessor } from "./otel"; import { createTracerProvider } from "../tests/utils"; -import { _exportsForTestingOnly } from "braintrust"; +import { _exportsForTestingOnly } from "../../../js/src/logger"; type OtlpTraceRequest = { resourceSpans?: Array<{ diff --git a/integrations/otel-js/src/id-gen.test.ts b/integrations/otel-js/src/id-gen.test.ts index b38e0afbc..19fcb6f93 100644 --- a/integrations/otel-js/src/id-gen.test.ts +++ b/integrations/otel-js/src/id-gen.test.ts @@ -1,6 +1,9 @@ import { expect, test, describe, beforeEach, afterEach } from "vitest"; -import { UUIDGenerator, getIdGenerator } from "braintrust"; -import { OTELIDGenerator } from "./otel"; +import { + OTELIDGenerator, + UUIDGenerator, + getIdGenerator, +} from "../../../js/src/id-gen"; import { setupOtelCompat, resetOtelCompat } from "."; describe("ID Generation", () => { diff --git a/integrations/otel-js/src/index.ts b/integrations/otel-js/src/index.ts index 9555cd32c..41f2b20cb 100644 --- a/integrations/otel-js/src/index.ts +++ b/integrations/otel-js/src/index.ts @@ -1,48 +1,20 @@ import { OtelContextManager } from "./context"; -import { _internalGetGlobalState } from "braintrust"; +import { configureContextManager } from "braintrust"; export { - contextFromSpanExport, + contextFromSpan, addSpanParentToBaggage, addParentToBaggage, parentFromHeaders, isRootSpan, } from "./otel"; -import { OTELIDGenerator } from "./otel"; -import { SpanComponentsV4 } from "braintrust/util"; - export { BraintrustSpanProcessor, BraintrustExporter } from "./otel"; -function resetBraintrustCompatCaches() { - const state = _internalGetGlobalState(); - if (!state) { - return; - } - - // Node/browser package initialization can cache native context and ID state - // before setupOtelCompat() runs. Reset both so subsequent lookups honor the - // compat-mode globals we are about to install. - (state as unknown as { _contextManager: unknown })._contextManager = null; - (state as unknown as { _idGenerator: unknown })._idGenerator = null; -} - export const setupOtelCompat = () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_CONTEXT_MANAGER = OtelContextManager; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_ID_GENERATOR = OTELIDGenerator; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_SPAN_COMPONENT = SpanComponentsV4; - resetBraintrustCompatCaches(); + configureContextManager(OtelContextManager); }; export const resetOtelCompat = () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_CONTEXT_MANAGER = undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_ID_GENERATOR = undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions - (globalThis as any).BRAINTRUST_SPAN_COMPONENT = undefined; - resetBraintrustCompatCaches(); + configureContextManager(undefined); }; diff --git a/integrations/otel-js/src/otel-compat.test.ts b/integrations/otel-js/src/otel-compat.test.ts index d06d9f843..35db904c6 100644 --- a/integrations/otel-js/src/otel-compat.test.ts +++ b/integrations/otel-js/src/otel-compat.test.ts @@ -6,14 +6,13 @@ */ import { beforeEach, afterEach, describe, expect, test } from "vitest"; +import { initLogger, currentSpan, Eval } from "braintrust"; import { - initLogger, - currentSpan, - getContextManager, - BRAINTRUST_CURRENT_SPAN_STORE, _exportsForTestingOnly, - Eval, -} from "braintrust"; + _internalStartSpan, + _internalWithParent, + getContextManager, +} from "../../../js/src/logger"; import { BasicTracerProvider, InMemorySpanExporter, @@ -264,15 +263,13 @@ describe("OTEL compatibility mode", () => { expect(cm.getCurrentSpan).toBeDefined(); }); - test("uses OtelContextManager when OTEL enabled", async () => { + test("uses OtelContextManager when OTEL enabled", () => { // Test that when OTEL is available and env var is set, we get OtelContextManager // Note: In test environment, OTEL packages may not be available even though // we checked OTEL_AVAILABLE. If the require fails in getContextManager, // it falls back to BraintrustContextManager, which is correct behavior. - // Clear module cache and re-import to get fresh context manager - const loggerModule = await import("braintrust?t=" + Date.now()); - const cm = loggerModule.getContextManager(); + const cm = getContextManager(); // If OTEL is truly available, we should get OtelContextManager // Otherwise, fallback to BraintrustContextManager is acceptable @@ -652,7 +649,7 @@ describe("OTEL compatibility mode", () => { parentSpan.end(); - const childSpan = logger.startSpan({ + const childSpan = _internalStartSpan({ name: "child-span-v4", parent: exported, }); @@ -678,11 +675,9 @@ describe("OTEL compatibility mode", () => { parentSpan.end(); // Use withParent helper with logger.startSpan to properly handle the exported parent - const { withParent } = await import("braintrust"); - - withParent(exported, () => { + _internalWithParent(exported, () => { // Pass parent explicitly in addition to withParent context - const childSpan = logger.startSpan({ + const childSpan = _internalStartSpan({ name: "child-span-v4-with-parent", parent: exported, }); @@ -720,12 +715,11 @@ describe("OtelContextManager TracingChannel integration", () => { resetOtelCompat(); }); - test("OtelContextManager exposes OTEL ALS via BRAINTRUST_CURRENT_SPAN_STORE", () => { + test("OtelContextManager exposes the OTEL ALS", () => { const cm = getContextManager(); expect(cm.constructor.name).toBe("OtelContextManager"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; expect(store).toBeDefined(); expect(typeof store.run).toBe("function"); expect(typeof store.getStore).toBe("function"); @@ -756,8 +750,7 @@ describe("OtelContextManager TracingChannel integration", () => { test("store.run() with wrapSpanForStore output propagates span to currentSpan()", () => { const cm = getContextManager(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; expect(store).toBeDefined(); const mockSpan = { @@ -778,8 +771,7 @@ describe("OtelContextManager TracingChannel integration", () => { test("nested store.run() calls maintain correct span chain", () => { const cm = getContextManager(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; const parentSpan = { spanId: "1111111111111111", @@ -863,10 +855,9 @@ describe("OtelContextManager fallback ALS (AsyncHooksContextManager)", () => { resetOtelCompat(); }); - test("exposes fallback ALS via BRAINTRUST_CURRENT_SPAN_STORE when OTEL ALS is unavailable", () => { + test("exposes fallback ALS when OTEL ALS is unavailable", () => { const cm = getContextManager(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; expect(store).toBeDefined(); expect(typeof store.run).toBe("function"); expect(typeof store.getStore).toBe("function"); @@ -889,8 +880,7 @@ describe("OtelContextManager fallback ALS (AsyncHooksContextManager)", () => { test("store.run() with span in fallback mode makes getCurrentSpan() work", () => { const cm = getContextManager(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const store = (cm as any)[BRAINTRUST_CURRENT_SPAN_STORE]; + const store = cm.getCurrentSpanStore()!; const mockSpan = { spanId: "fedcba0987654321", @@ -938,29 +928,16 @@ describe("Distributed Tracing (BT → OTEL)", () => { resetOtelCompat(); }); - test("otelContextFromSpanExport parses BT span and creates OTEL context", async () => { - const { contextFromSpanExport: otelContextFromSpanExport } = - await import("./"); - const { SpanComponentsV4 } = await import("braintrust/util"); - const { SpanObjectTypeV3 } = await import("braintrust/util"); - - // Create a sample span export string + test("contextFromSpan creates an OTEL context", async () => { + const { contextFromSpan } = await import("./"); const rootSpanId = "a1b2c3d4e5f6789012345678abcdef01"; // 32 hex chars (16 bytes) const spanId = "a1b2c3d4e5f67890"; // 16 hex chars (8 bytes) - const objectId = "proj-123"; - - const components = new SpanComponentsV4({ - object_type: SpanObjectTypeV3.PROJECT_LOGS, - object_id: objectId, - row_id: "row-123", - span_id: spanId, - root_span_id: rootSpanId, - propagated_event: undefined, - }); - - const exportStr = components.toStr(); - - const ctx = otelContextFromSpanExport(exportStr); + const ctx = contextFromSpan({ + inject: () => ({ + traceparent: `00-${rootSpanId}-${spanId}-01`, + baggage: "braintrust.parent=project_id%3Aproj-123", + }), + } as any); // Verify that a valid context was created expect(ctx).toBeDefined(); @@ -985,8 +962,7 @@ describe("Distributed Tracing (BT → OTEL)", () => { test("BT span in Service A can be parent of OTEL span in Service B", async () => { const { tracer, exporter, processor } = setupOtelFixture("service-a-project"); - const { contextFromSpanExport: otelContextFromSpanExport } = - await import("./"); + const { contextFromSpan } = await import("./"); const projectName = "service-a-project"; const logger = initLogger({ projectName }); @@ -994,25 +970,23 @@ describe("Distributed Tracing (BT → OTEL)", () => { // ===== Service A: Create BT span and export ===== let serviceATraceId: string | undefined; let serviceASpanId: string | undefined; - let exportedContext: string | undefined; + let sourceSpan: import("braintrust").Span | undefined; await logger.traced( async (serviceASpan) => { serviceATraceId = serviceASpan.rootSpanId; serviceASpanId = serviceASpan.spanId; - - // Export context for sending to Service B (e.g., via HTTP header) - exportedContext = await serviceASpan.export(); + sourceSpan = serviceASpan; }, { name: "service_a_span" }, ); - expect(exportedContext).toBeDefined(); + expect(sourceSpan).toBeDefined(); expect(serviceATraceId).toBeDefined(); expect(serviceASpanId).toBeDefined(); // ===== Service B: Import context and create OTEL child span ===== - const ctx = otelContextFromSpanExport(exportedContext!); + const ctx = contextFromSpan(sourceSpan!); // Use context.with() to run code in the imported context await context.with(ctx, async () => { diff --git a/integrations/otel-js/src/otel.test.ts b/integrations/otel-js/src/otel.test.ts index 7522450da..e34e2a36b 100644 --- a/integrations/otel-js/src/otel.test.ts +++ b/integrations/otel-js/src/otel.test.ts @@ -26,13 +26,14 @@ import { parentFromHeaders, isRootSpan, } from "./otel"; -import { _exportsForTestingOnly, initLogger } from "braintrust"; +import { initLogger } from "braintrust"; +import { SpanComponentsV3, SpanComponentsV4 } from "../../../js/util"; +import { _exportsForTestingOnly } from "../../../js/src/logger"; import { base64ToUint8Array, getExportVersion, createTracerProvider, } from "../tests/utils"; -import { SpanComponentsV3, SpanComponentsV4 } from "braintrust/util"; import { setupOtelCompat, resetOtelCompat } from "."; const packageJson = JSON.parse( @@ -61,6 +62,13 @@ async function withEmptyBraintrustEnvFile( return withBraintrustEnvFile("", fn); } +function restoreEnvironment(snapshot: NodeJS.ProcessEnv): void { + for (const name of Object.keys(process.env)) { + if (!(name in snapshot)) delete process.env[name]; + } + Object.assign(process.env, snapshot); +} + describe("AISpanProcessor", () => { let memoryExporter: InMemorySpanExporter; let provider: BasicTracerProvider; @@ -605,7 +613,7 @@ describe("BraintrustSpanProcessor", () => { }); afterEach(() => { - process.env = originalEnv; + restoreEnvironment(originalEnv); vi.restoreAllMocks(); _exportsForTestingOnly.clearTestBackgroundLogger(); _exportsForTestingOnly.simulateLogoutForTests(); @@ -1008,7 +1016,7 @@ describe("BraintrustExporter", () => { }); afterEach(() => { - process.env = originalEnv; + restoreEnvironment(originalEnv); vi.restoreAllMocks(); _exportsForTestingOnly.clearTestBackgroundLogger(); _exportsForTestingOnly.simulateLogoutForTests(); @@ -1392,180 +1400,35 @@ describe("otel namespace helpers", () => { }); describe("parentFromHeaders", () => { - describe("valid inputs", () => { - it("should extract parent from headers with valid traceparent and braintrust.parent baggage", () => { - const headers = { - traceparent: - "00-12345678901234567890123456789012-1234567890123456-01", - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeDefined(); - // Parent string is base64-encoded SpanComponentsV4 - expect(typeof parent).toBe("string"); - expect(parent!.length).toBeGreaterThan(0); - }); - - it("should extract parent with project_id", () => { - const headers = { - traceparent: - "00-abcdef12345678901234567890123456-fedcba9876543210-01", - baggage: "braintrust.parent=project_id:my-project-id", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeDefined(); - expect(typeof parent).toBe("string"); - expect(parent!.length).toBeGreaterThan(0); - }); - - it("should extract parent with experiment_id", () => { - const headers = { - traceparent: - "00-11111111111111111111111111111111-2222222222222222-01", - baggage: "braintrust.parent=experiment_id:my-experiment-id", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeDefined(); - expect(typeof parent).toBe("string"); - expect(parent!.length).toBeGreaterThan(0); - }); + it("returns the opaque W3C context", () => { + const headers = { + traceparent: "00-12345678901234567890123456789012-1234567890123456-01", + tracestate: "vendor=value", + baggage: "braintrust.parent=project_name%3Atest", + }; + expect(parentFromHeaders(headers)).toEqual(headers); }); - describe("invalid inputs", () => { - it("should return undefined when traceparent is missing", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalledWith( - "parentFromHeaders: No valid span context in headers", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when baggage is missing", () => { - const consoleSpy = vi - .spyOn(console, "warn") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-12345678901234567890123456789012-1234567890123456-01", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalled(); - expect(consoleSpy.mock.calls[0][0]).toContain( - "braintrust.parent not found", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when braintrust.parent is missing from baggage", () => { - const consoleSpy = vi - .spyOn(console, "warn") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-12345678901234567890123456789012-1234567890123456-01", - baggage: "foo=bar,baz=qux", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalled(); - expect(consoleSpy.mock.calls[0][0]).toContain( - "braintrust.parent not found", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when traceparent format is invalid", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - traceparent: "invalid-traceparent", - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalledWith( - "parentFromHeaders: No valid span context in headers", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when trace_id is all zeros", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-00000000000000000000000000000000-1234567890123456-01", - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - // OTEL's extract() validates and rejects invalid trace_id - expect(consoleSpy).toHaveBeenCalledWith( - "parentFromHeaders: No valid span context in headers", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when span_id is all zeros", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-12345678901234567890123456789012-0000000000000000-01", - baggage: "braintrust.parent=project_name:test", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - // OTEL's extract() validates and rejects invalid span_id - expect(consoleSpy).toHaveBeenCalledWith( - "parentFromHeaders: No valid span context in headers", - ); - - consoleSpy.mockRestore(); - }); - - it("should return undefined when braintrust.parent format is invalid", () => { - const consoleSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - const headers = { - traceparent: - "00-12345678901234567890123456789012-1234567890123456-01", - baggage: "braintrust.parent=invalid", - }; - - const parent = parentFromHeaders(headers); - expect(parent).toBeUndefined(); - // Should reach our validation if span context is valid, otherwise OTEL rejects it - expect(consoleSpy).toHaveBeenCalled(); + it("keeps a valid trace context without Braintrust baggage", () => { + const headers = { + traceparent: "00-12345678901234567890123456789012-1234567890123456-01", + }; + expect(parentFromHeaders(headers)).toEqual(headers); + }); - consoleSpy.mockRestore(); - }); + it.each([ + {}, + { traceparent: "invalid-traceparent" }, + { + traceparent: "00-00000000000000000000000000000000-1234567890123456-01", + }, + { + traceparent: "00-12345678901234567890123456789012-0000000000000000-01", + }, + ])("returns undefined for an invalid traceparent", (headers) => { + expect( + parentFromHeaders(headers as Record), + ).toBeUndefined(); }); }); }); @@ -1735,7 +1598,9 @@ describe("Otel Compat tests Integration", () => { expect(uuidSpan.spanId).toMatch(uuidRegex); uuidSpan.end(); - // Switch to OTEL compat (hex, wins over legacy). + // Switch to the default hex ID mode. OTEL compat only changes context + // management now; it does not override the core ID generator. + delete process.env.BRAINTRUST_LEGACY_IDS; setupOtelCompat(); _exportsForTestingOnly.resetIdGenStateForTests(); @@ -1941,26 +1806,6 @@ describe("export() format selection based on if otel is initialized", () => { expect(v4Parsed.data.object_type).toBeDefined(); }); - test("exported V4 span can be used as parent", async () => { - const testLogger = initLogger({ - projectName: "test-v4-parent", - apiKey: "test-key", - }); - - const parentSpan = testLogger.startSpan({ name: "parent-span-v4" }); - const exported = await parentSpan.export(); - parentSpan.end(); - - // Should be able to use V4 exported string as parent - const childSpan = testLogger.startSpan({ - name: "child-span-v4", - parent: exported, - }); - - expect(childSpan.rootSpanId).toBe(parentSpan.rootSpanId); - childSpan.end(); - }); - test("V4 format uses hex IDs (not UUIDs) when otel is initialized", async () => { _exportsForTestingOnly.resetIdGenStateForTests(); diff --git a/integrations/otel-js/src/otel.ts b/integrations/otel-js/src/otel.ts index dc7e23aca..bcc817017 100644 --- a/integrations/otel-js/src/otel.ts +++ b/integrations/otel-js/src/otel.ts @@ -1,14 +1,19 @@ -import { SpanComponentsV4, SpanObjectTypeV3 } from "braintrust/util"; +import { registerOtelFlush } from "braintrust/instrumentation"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { + CompositePropagator, + W3CBaggagePropagator, + W3CTraceContextPropagator, +} from "@opentelemetry/core"; import { context, Context, diag, trace, - TraceFlags, propagation, Span, + type TextMapGetter, } from "@opentelemetry/api"; import { SpanProcessor, @@ -18,10 +23,9 @@ import { type SpanExporter, } from "@opentelemetry/sdk-trace-base"; import { - IDGenerator, - _internalIso, currentSpan, - registerOtelFlush, + extractTraceContextFromHeaders, + type PropagationContext, type Span as BraintrustSpan, } from "braintrust"; @@ -45,6 +49,14 @@ const SYSTEM_ATTRIBUTE_NAMES = new Set([ "braintrust.context_json", ]); +const braintrustW3CPropagator = new CompositePropagator({ + propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()], +}); +const traceContextGetter: TextMapGetter> = { + keys: Object.keys, + get: (carrier, key) => carrier[key], +}; + /** * Custom filter function type for span filtering. * @param span - The span to evaluate @@ -277,45 +289,100 @@ function withSpanOriginAttributes( }); } +const BRAINTRUST_ENV_SEARCH_PARENT_LIMIT = 64; + +function getEnv(name: string): string | undefined { + if (typeof process === "undefined") return undefined; + const value = process.env[name]; + if (value?.trim()) return value; + if ( + name !== "BRAINTRUST_API_KEY" && + name !== "BRAINTRUST_ENVIRONMENT_TYPE" && + name !== "BRAINTRUST_ENVIRONMENT_NAME" + ) { + return value; + } + + if (typeof process.loadEnvFile !== "function") return undefined; + + let dir: string; + try { + dir = process.cwd(); + } catch { + return undefined; + } + + for (let depth = 0; depth <= BRAINTRUST_ENV_SEARCH_PARENT_LIMIT; depth++) { + try { + const separator = dir.includes("\\") ? "\\" : "/"; + process.loadEnvFile( + `${dir}${dir.endsWith(separator) ? "" : separator}.env.braintrust`, + ); + const fileValue = process.env[name]; + return fileValue?.trim() ? fileValue : undefined; + } catch (error) { + if ( + typeof error !== "object" || + error === null || + !("code" in error) || + error.code !== "ENOENT" + ) { + return undefined; + } + } + + const trimmed = dir.replace(/[\\/]+$/, ""); + const separatorIndex = Math.max( + trimmed.lastIndexOf("/"), + trimmed.lastIndexOf("\\"), + ); + const parent = + separatorIndex < 0 + ? dir + : separatorIndex === 0 + ? trimmed.slice(0, 1) + : trimmed.slice(0, separatorIndex); + if (parent === dir) break; + dir = parent; + } + return undefined; +} + function detectEnvironment(explicit?: { type?: string; name?: string; }): { type?: string; name?: string } | undefined { if (explicit) return explicit; - const envType = _internalIso.getEnv("BRAINTRUST_ENVIRONMENT_TYPE"); - const envName = _internalIso.getEnv("BRAINTRUST_ENVIRONMENT_NAME"); + const envType = getEnv("BRAINTRUST_ENVIRONMENT_TYPE"); + const envName = getEnv("BRAINTRUST_ENVIRONMENT_NAME"); if (envType || envName) { return { ...(envType ? { type: envType } : {}), ...(envName ? { name: envName } : {}), }; } - if (_internalIso.getEnv("GITHUB_ACTIONS")) - return { type: "ci", name: "github_actions" }; - if (_internalIso.getEnv("GITLAB_CI")) - return { type: "ci", name: "gitlab_ci" }; - if (_internalIso.getEnv("CIRCLECI")) return { type: "ci", name: "circleci" }; - if (_internalIso.getEnv("BUILDKITE")) - return { type: "ci", name: "buildkite" }; - if (_internalIso.getEnv("CI")) return { type: "ci", name: "ci" }; - if (_internalIso.getEnv("VERCEL")) return { type: "server", name: "vercel" }; - if (_internalIso.getEnv("NETLIFY")) - return { type: "server", name: "netlify" }; - const awsExecutionEnv = _internalIso.getEnv("AWS_EXECUTION_ENV"); + if (getEnv("GITHUB_ACTIONS")) return { type: "ci", name: "github_actions" }; + if (getEnv("GITLAB_CI")) return { type: "ci", name: "gitlab_ci" }; + if (getEnv("CIRCLECI")) return { type: "ci", name: "circleci" }; + if (getEnv("BUILDKITE")) return { type: "ci", name: "buildkite" }; + if (getEnv("CI")) return { type: "ci", name: "ci" }; + if (getEnv("VERCEL")) return { type: "server", name: "vercel" }; + if (getEnv("NETLIFY")) return { type: "server", name: "netlify" }; + const awsExecutionEnv = getEnv("AWS_EXECUTION_ENV"); if ( - _internalIso.getEnv("ECS_CONTAINER_METADATA_URI") || - _internalIso.getEnv("ECS_CONTAINER_METADATA_URI_V4") || + getEnv("ECS_CONTAINER_METADATA_URI") || + getEnv("ECS_CONTAINER_METADATA_URI_V4") || awsExecutionEnv?.startsWith("AWS_ECS_") ) { return { type: "server", name: "ecs" }; } if ( - _internalIso.getEnv("AWS_LAMBDA_FUNCTION_NAME") || + getEnv("AWS_LAMBDA_FUNCTION_NAME") || awsExecutionEnv?.startsWith("AWS_Lambda_") ) { return { type: "server", name: "aws_lambda" }; } - const nodeEnv = _internalIso.getEnv("NODE_ENV"); + const nodeEnv = getEnv("NODE_ENV"); if (!nodeEnv) return undefined; const normalizedNodeEnv = nodeEnv.toLowerCase(); if (normalizedNodeEnv === "production" || normalizedNodeEnv === "staging") { @@ -438,7 +505,7 @@ class LazyBraintrustOTLPTraceExporter implements SpanExporter { const apiKey = this.apiKey !== undefined ? this.apiKey - : await _internalIso.getBraintrustApiKey(); + : getEnv("BRAINTRUST_API_KEY"); if (!apiKey?.trim()) { throw new Error( "Braintrust API key is required. Set BRAINTRUST_API_KEY, define it in .env.braintrust, or pass apiKey option.", @@ -529,7 +596,7 @@ export class BraintrustSpanProcessor implements SpanProcessor { const apiKey = options.apiKey !== undefined ? options.apiKey - : _internalIso.getEnv("BRAINTRUST_API_KEY"); + : getEnv("BRAINTRUST_API_KEY"); // Get API URL from options or environment let apiUrl = @@ -668,132 +735,13 @@ export class BraintrustSpanProcessor implements SpanProcessor { } } -/** - * Create an OTEL context from a Braintrust span export string. - * - * Used for distributed tracing scenarios where a Braintrust span in one service - * needs to be the parent of an OTEL span in another service. - * - * @param exportStr - The string returned from span.export() - * @returns OTEL context that can be used when creating child spans - * - * @example - * ```typescript - * // Service A: Create BT span and export - * const span = logger.startSpan({ name: "service-a" }); - * const exportStr = await span.export(); - * // Send exportStr to Service B (e.g., via HTTP header) - * - * // Service B: Import context and create OTEL child - * import * as api from '@opentelemetry/api'; - * const ctx = contextFromSpanExport(exportStr); - * await api.context.with(ctx, async () => { - * await tracer.startActiveSpan("service-b", async (span) => { - * // This span is now a child of the Service A span - * span.end(); - * }); - * }); - * ``` - */ -export function contextFromSpanExport(exportStr: string): unknown { - // Parse the export string - const components = SpanComponentsV4.fromStr(exportStr); - - // Get trace and span IDs (already in hex format) - const traceIdHex = components.data.root_span_id; // 32 hex chars - const spanIdHex = components.data.span_id; // 16 hex chars - - if (!traceIdHex || !spanIdHex) { - throw new Error( - "Export string must contain root_span_id and span_id for distributed tracing", - ); - } - - // Create SpanContext marked as remote (critical for distributed tracing) - const spanContext = { - traceId: traceIdHex, - spanId: spanIdHex, - isRemote: true, - traceFlags: TraceFlags?.SAMPLED ?? 1, // SAMPLED flag - }; - - // Create NonRecordingSpan using wrapSpanContext and set in context - const nonRecordingSpan = trace.wrapSpanContext(spanContext); - let ctx = trace.setSpan(context.active(), nonRecordingSpan); - - // Construct braintrust.parent identifier - const braintrustParent = getBraintrustParent( - components.data.object_type, - components.data.object_id, - components.data.compute_object_metadata_args, +/** Create an OTEL context from a Braintrust span using W3C propagation. */ +export function contextFromSpan(span: BraintrustSpan): Context { + return braintrustW3CPropagator.extract( + context.active(), + span.inject(), + traceContextGetter, ); - - // Set braintrust.parent in baggage so it propagates automatically - if (braintrustParent) { - try { - // Try to set baggage if available - if (propagation) { - const baggage = - propagation.getBaggage(ctx) || propagation.createBaggage(); - ctx = propagation.setBaggage( - ctx, - baggage.setEntry("braintrust.parent", { - value: braintrustParent, - }), - ); - } - } catch (error) { - console.error( - "Failed to set braintrust.parent in baggage during context import:", - error, - ); - } - } - - return ctx; -} - -/** - * Construct a braintrust.parent identifier string from span components. - * - * @param objectType - Type of parent object (PROJECT_LOGS or EXPERIMENT) - * @param objectId - Resolved object ID (project_id or experiment_id) - * @param computeArgs - Optional dict with project_name/project_id for unresolved cases - * @returns String like "project_id:abc", "project_name:my-proj", "experiment_id:exp-123", or undefined - */ -function getBraintrustParent( - objectType: number, - objectId?: string | null, - computeArgs?: Record | null, -): string | undefined { - if (!objectType) { - return undefined; - } - - if (objectType === SpanObjectTypeV3.PROJECT_LOGS) { - if (objectId) { - return `project_id:${objectId}`; - } else if (computeArgs) { - const projectId = computeArgs["project_id"]; - const projectName = computeArgs["project_name"]; - if (typeof projectId === "string") { - return `project_id:${projectId}`; - } else if (typeof projectName === "string") { - return `project_name:${projectName}`; - } - } - } else if (objectType === SpanObjectTypeV3.EXPERIMENT) { - if (objectId) { - return `experiment_id:${objectId}`; - } else if (computeArgs) { - const experimentId = computeArgs["experiment_id"]; - if (typeof experimentId === "string") { - return `experiment_id:${experimentId}`; - } - } - } - - return undefined; } /** @@ -806,46 +754,16 @@ function getBraintrustParent( export function getOtelParentFromSpan( span: BraintrustSpan, ): string | undefined { - const parent = span.getParentInfo(); - - if (!parent || !parent.objectType || !parent.objectId) { - return undefined; - } - try { - if (parent.objectType === SpanObjectTypeV3.PROJECT_LOGS) { - const syncResult = parent.objectId.getSync(); - const id = syncResult?.value; - const args = parent.computeObjectMetadataArgs; - - if (id) { - return `project_id:${id}`; - } - - const projectName = args?.project_name; - if (typeof projectName === "string") { - return `project_name:${projectName}`; - } - } else if (parent.objectType === SpanObjectTypeV3.EXPERIMENT) { - const syncResult = parent.objectId.getSync(); - const id = syncResult?.value; - - // Debug details for experiment - // eslint-disable-next-line no-console - console.debug("[getOtelParentFromSpan] EXPERIMENT", { id }); - - if (id) { - // eslint-disable-next-line no-console - console.debug( - "[getOtelParentFromSpan] EXPERIMENT using experiment_id", - { id }, - ); - return `experiment_id:${id}`; + if (typeof span.inject !== "function") return undefined; + const baggage = span.inject().baggage; + for (const member of baggage?.split(",") ?? []) { + const [rawKey, rawValue] = member.trim().split("=", 2); + if (rawKey === "braintrust.parent" && rawValue) { + return decodeURIComponent(rawValue.split(";", 1)[0]); } } } catch (e) { - // Debug: unexpected error reading parent info - // eslint-disable-next-line no-console console.warn("[getOtelParentFromSpan] error extracting parent", e); } return undefined; @@ -1045,188 +963,9 @@ export function addSpanParentToBaggage( return addParentToBaggage(parentValue, ctx); } -/** - * Extract a Braintrust-compatible parent string from W3C Trace Context headers. - * - * This converts OTEL trace context headers (traceparent/baggage) into a format - * that can be passed as the 'parent' parameter to Braintrust's traced() method. - * - * @param headers - Dictionary with 'traceparent' and optionally 'baggage' keys - * @returns Braintrust V4 export string that can be used as parent parameter, - * or undefined if no valid span context is found or braintrust.parent is missing. - * - * @example - * ```typescript - * import { initLogger } from "braintrust"; - * import { parentFromHeaders } from "@braintrust/otel"; - * - * // Service C receives headers from Service B - * const headers = { traceparent: '00-trace_id-span_id-01', baggage: '...' }; - * const parent = parentFromHeaders(headers); - * - * const logger = initLogger({ projectName: "my-project" }); - * await logger.traced(async (span) => { - * span.log({ input: "BT span as child of OTEL parent" }); - * }, { name: "service_c", parent }); - * ``` - */ +/** Extract an opaque Braintrust parent context from W3C headers. */ export function parentFromHeaders( headers: Record, -): string | undefined { - try { - if (!propagation) { - console.error("OTEL propagation API not available"); - return undefined; - } - - // Extract context from headers using W3C Trace Context propagator - // This parses both traceparent and baggage headers - const ctx = propagation.extract(context.active(), headers); - - // Get span context directly from the extracted context - const spanContext = trace.getSpanContext(ctx); - if (!spanContext) { - console.error("parentFromHeaders: No valid span context in headers"); - return undefined; - } - - // Get trace_id and span_id from span context - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const traceIdHex = spanContext.traceId as string; - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const spanIdHex = spanContext.spanId as string; - - // Validate trace_id and span_id are not all zeros - if ( - !traceIdHex || - typeof traceIdHex !== "string" || - traceIdHex === "00000000000000000000000000000000" - ) { - console.error("parentFromHeaders: Invalid trace_id (all zeros)"); - return undefined; - } - if ( - !spanIdHex || - typeof spanIdHex !== "string" || - spanIdHex === "0000000000000000" - ) { - console.error("parentFromHeaders: Invalid span_id (all zeros)"); - return undefined; - } - - // Get braintrust.parent from baggage - const baggage = propagation.getBaggage(ctx); - const braintrustParent = baggage?.getEntry("braintrust.parent")?.value; - - if (!braintrustParent) { - console.warn( - "parentFromHeaders: braintrust.parent not found in OTEL baggage. " + - "Cannot create Braintrust parent without project information. " + - "Ensure the OTEL span sets braintrust.parent in baggage before exporting headers.", - ); - return undefined; - } - - // Parse braintrust.parent to extract object_type and object_id - let objectType: number | undefined; - let objectId: string | undefined; - let computeArgs: Record | undefined; - - // Parse braintrust.parent format: "project_id:abc", "project_name:xyz", or "experiment_id:123" - if (braintrustParent.startsWith("project_id:")) { - objectType = SpanObjectTypeV3.PROJECT_LOGS; - objectId = braintrustParent.substring("project_id:".length); - if (!objectId) { - console.error( - `parentFromHeaders: Invalid braintrust.parent format (empty project_id): ${braintrustParent}`, - ); - return undefined; - } - } else if (braintrustParent.startsWith("project_name:")) { - objectType = SpanObjectTypeV3.PROJECT_LOGS; - const projectName = braintrustParent.substring("project_name:".length); - if (!projectName) { - console.error( - `parentFromHeaders: Invalid braintrust.parent format (empty project_name): ${braintrustParent}`, - ); - return undefined; - } - computeArgs = { project_name: projectName }; - } else if (braintrustParent.startsWith("experiment_id:")) { - objectType = SpanObjectTypeV3.EXPERIMENT; - objectId = braintrustParent.substring("experiment_id:".length); - if (!objectId) { - console.error( - `parentFromHeaders: Invalid braintrust.parent format (empty experiment_id): ${braintrustParent}`, - ); - return undefined; - } - } else { - console.error( - `parentFromHeaders: Invalid braintrust.parent format: ${braintrustParent}. ` + - "Expected format: 'project_id:ID', 'project_name:NAME', or 'experiment_id:ID'", - ); - return undefined; - } - - // Create SpanComponentsV4 and export as string - const componentsData: { - object_type: number; - object_id?: string | null; - compute_object_metadata_args?: Record | null; - row_id: string; - span_id: string; - root_span_id: string; - } = { - object_type: objectType, - row_id: "otel", // Dummy row_id to enable span_id/root_span_id fields - span_id: spanIdHex, - root_span_id: traceIdHex, - }; - - // Add either object_id or compute_object_metadata_args, not both - if (computeArgs) { - componentsData.compute_object_metadata_args = computeArgs; - } else { - componentsData.object_id = objectId; - } - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any - const components = new SpanComponentsV4(componentsData as any); - - return components.toStr(); - } catch (error) { - console.error("parentFromHeaders: Error parsing headers:", error); - return undefined; - } -} - -function generateHexId(bytes: number): string { - let result = ""; - for (let i = 0; i < bytes; i++) { - result += Math.floor(Math.random() * 256) - .toString(16) - .padStart(2, "0"); - } - return result; -} - -/** - * ID generator that generates OpenTelemetry-compatible IDs - * Uses hex strings for compatibility with OpenTelemetry systems - */ -export class OTELIDGenerator extends IDGenerator { - getSpanId(): string { - // Generate 8 random bytes and convert to hex (16 characters) - return generateHexId(8); - } - - getTraceId(): string { - // Generate 16 random bytes and convert to hex (32 characters) - return generateHexId(16); - } - - shareRootSpanId(): boolean { - return false; - } +): PropagationContext | undefined { + return extractTraceContextFromHeaders(headers); } diff --git a/integrations/otel-js/tests/utils.ts b/integrations/otel-js/tests/utils.ts index 95d30b3b9..a435f7515 100644 --- a/integrations/otel-js/tests/utils.ts +++ b/integrations/otel-js/tests/utils.ts @@ -139,7 +139,20 @@ export function logOtelVersions(version: "v1" | "v2" | "parent"): void { * which is critical for testing with different OpenTelemetry versions. */ export function createOtelAliases(cwd: string): Record { + const repositoryRoot = + detectOtelVersion(cwd) === "parent" + ? path.resolve(cwd, "../..") + : path.resolve(cwd, "../../.."); return { + "@braintrust/otel": path.resolve( + repositoryRoot, + "integrations/otel-js/src/index.ts", + ), + "braintrust/instrumentation": path.resolve( + repositoryRoot, + "js/src/instrumentation/index.ts", + ), + braintrust: path.resolve(repositoryRoot, "js/src/node/index.ts"), "@opentelemetry/api": path.resolve(cwd, "node_modules/@opentelemetry/api"), "@opentelemetry/core": path.resolve( cwd, diff --git a/integrations/otel-js/tsconfig.json b/integrations/otel-js/tsconfig.json index 8cfc6d9ae..352d6b9f7 100644 --- a/integrations/otel-js/tsconfig.json +++ b/integrations/otel-js/tsconfig.json @@ -2,9 +2,9 @@ "compilerOptions": { "declaration": true, "lib": ["es2022"], - "module": "commonjs", + "module": "esnext", "target": "es2022", - "moduleResolution": "node", + "moduleResolution": "bundler", "strict": true, "noUnusedLocals": true, "esModuleInterop": true, diff --git a/integrations/otel-js/tsconfig.test.json b/integrations/otel-js/tsconfig.test.json index eff2845e0..ad0f1bdc1 100644 --- a/integrations/otel-js/tsconfig.test.json +++ b/integrations/otel-js/tsconfig.test.json @@ -6,7 +6,9 @@ "types": ["node"], "baseUrl": ".", "paths": { - "@braintrust/otel": ["src/index.ts"] + "@braintrust/otel": ["src/index.ts"], + "braintrust": ["../../js/src/node/index.ts"], + "braintrust/instrumentation": ["../../js/src/instrumentation/index.ts"] } }, "include": ["src/**/*.test.ts", "tests/**/*.ts"], diff --git a/integrations/otel-js/vitest.config.ts b/integrations/otel-js/vitest.config.ts index 5da49856a..bf4fb3a75 100644 --- a/integrations/otel-js/vitest.config.ts +++ b/integrations/otel-js/vitest.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from "vitest/config"; import { readFileSync } from "node:fs"; +import { createOtelAliases } from "./tests/utils"; const packageJson = JSON.parse( readFileSync(new URL("./package.json", import.meta.url), "utf8"), @@ -9,6 +10,7 @@ export default defineConfig({ define: { __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), }, + resolve: { alias: createOtelAliases(process.cwd()) }, test: { reporters: ["default"], include: ["tests/**/*.test.ts", "src/**/*.test.ts"], diff --git a/integrations/temporal-js/src/interceptors.ts b/integrations/temporal-js/src/interceptors.ts index 70706872d..64d09dfc7 100644 --- a/integrations/temporal-js/src/interceptors.ts +++ b/integrations/temporal-js/src/interceptors.ts @@ -6,15 +6,9 @@ import type { ActivityInterceptors, } from "@temporalio/worker"; import type { WorkflowClientInterceptor } from "@temporalio/client"; -import { defaultPayloadConverter } from "@temporalio/common"; import * as braintrust from "braintrust"; -import { SpanComponentsV4 } from "braintrust/util"; -import { getWorkflowSpanExport } from "./sinks"; -import { - BRAINTRUST_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_ID_HEADER, - deserializeHeaderValue, -} from "./utils"; +import { getWorkflowTraceContext } from "./sinks"; +import { deserializeTraceContext, serializeTraceContext } from "./utils"; /** * Create a client interceptor that propagates Braintrust span context to workflows. @@ -25,19 +19,13 @@ export function createBraintrustClientInterceptor(): WorkflowClientInterceptor { async start(input, next) { const span = braintrust.currentSpan(); if (span) { - const exported = await span.export(); - if (exported) { - const payload = defaultPayloadConverter.toPayload(exported); - if (payload) { - return next({ - ...input, - headers: { - ...input.headers, - [BRAINTRUST_SPAN_HEADER]: payload, - }, - }); - } - } + return next({ + ...input, + headers: { + ...input.headers, + ...serializeTraceContext(braintrust.injectTraceContext()), + }, + }); } return next(input); }, @@ -47,19 +35,13 @@ export function createBraintrustClientInterceptor(): WorkflowClientInterceptor { async signalWithStart(input, next) { const span = braintrust.currentSpan(); if (span) { - const exported = await span.export(); - if (exported) { - const payload = defaultPayloadConverter.toPayload(exported); - if (payload) { - return next({ - ...input, - headers: { - ...input.headers, - [BRAINTRUST_SPAN_HEADER]: payload, - }, - }); - } - } + return next({ + ...input, + headers: { + ...input.headers, + ...serializeTraceContext(braintrust.injectTraceContext()), + }, + }); } return next(input); }, @@ -79,76 +61,13 @@ class BraintrustActivityInterceptor implements ActivityInboundCallsInterceptor { const info = this.ctx.info; const runId = info.workflowExecution.runId; - // Try to get workflow span export - first check local Map, then headers - let parent: string | undefined; + // Prefer the live workflow span on the same worker, then fall back to the + // W3C context propagated through Temporal headers. + let parent: braintrust.PropagationContext | undefined; // Check if we have the workflow span export locally (same worker as workflow) - const spanExportPromise = getWorkflowSpanExport(runId); - if (spanExportPromise) { - try { - parent = await spanExportPromise; - } catch { - // Ignore errors, fall through to header check - } - } - - // For cross-worker activities: construct parent from workflow span ID + client context - if (!parent && input.headers) { - const workflowSpanId = deserializeHeaderValue( - input.headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER], - ); - const clientContext = deserializeHeaderValue( - input.headers[BRAINTRUST_SPAN_HEADER], - ); - - if (workflowSpanId && clientContext) { - try { - const clientComponents = SpanComponentsV4.fromStr(clientContext); - const clientData = clientComponents.data; - - // We can only construct a workflow parent if we have: - // 1. Tracing context (root_span_id) - // 2. Object metadata (object_id or compute_object_metadata_args) - const hasTracingContext = !!clientData.root_span_id; - const hasObjectMetadata = - !!clientData.object_id || !!clientData.compute_object_metadata_args; - - if (hasTracingContext && hasObjectMetadata) { - // Construct workflow parent with the workflow's span ID - // IMPORTANT: row_id must match span_id for the parent span - // Must provide EITHER object_id OR compute_object_metadata_args, not both - const workflowParentBase = { - object_type: clientData.object_type, - propagated_event: clientData.propagated_event, - row_id: workflowSpanId, // Use workflow's row_id, not client's - span_id: workflowSpanId, // Use workflow's span_id, not client's - root_span_id: clientData.root_span_id, // Keep same trace - }; - const workflowComponents = clientData.object_id - ? new SpanComponentsV4({ - ...workflowParentBase, - object_id: clientData.object_id, - }) - : new SpanComponentsV4({ - ...workflowParentBase, - compute_object_metadata_args: - clientData.compute_object_metadata_args!, - }); - - parent = workflowComponents.toStr(); - } else { - // Client context doesn't have root_span_id, use it directly - parent = clientContext; - } - } catch { - // Fall back to client context if parsing fails - parent = clientContext; - } - } else if (clientContext) { - // No workflow span ID, use client context directly - parent = clientContext; - } - } + parent = + getWorkflowTraceContext(runId) ?? deserializeTraceContext(input.headers); const span = braintrust.startSpan({ name: `temporal.activity.${info.activityType}`, diff --git a/integrations/temporal-js/src/sinks.ts b/integrations/temporal-js/src/sinks.ts index bd2967ca4..ee8dc8891 100644 --- a/integrations/temporal-js/src/sinks.ts +++ b/integrations/temporal-js/src/sinks.ts @@ -6,24 +6,26 @@ import * as braintrust from "braintrust"; // NOTE: WorkflowInfo is NOT included here - it's automatically injected by the runtime export interface BraintrustSinks extends Sinks { braintrust: { - workflowStarted(parentContext?: string, workflowSpanId?: string): void; + workflowStarted( + parentContext?: braintrust.PropagationContext, + workflowSpanId?: string, + ): void; workflowCompleted(error?: string): void; }; } // Active workflow spans tracked by run ID const workflowSpans = new Map(); -// Workflow span exports tracked by run ID (as promises for async export) -const workflowSpanExports = new Map>(); +const workflowTraceContexts = new Map(); /** * Get the exported span context for a workflow by run ID. * Activities on the same worker can use this to parent to the workflow span. */ -export function getWorkflowSpanExport( +export function getWorkflowTraceContext( runId: string, -): Promise | undefined { - return workflowSpanExports.get(runId); +): braintrust.PropagationContext | undefined { + return workflowTraceContexts.get(runId); } /** @@ -36,7 +38,7 @@ export function createBraintrustSinks(): InjectedSinks { workflowStarted: { fn: ( info: WorkflowInfo, - parentContext?: string, + parentContext?: braintrust.PropagationContext, workflowSpanId?: string, ) => { const span = braintrust.startSpan({ @@ -53,7 +55,7 @@ export function createBraintrustSinks(): InjectedSinks { }, }); workflowSpans.set(info.runId, span); - workflowSpanExports.set(info.runId, span.export()); + workflowTraceContexts.set(info.runId, span.inject()); }, callDuringReplay: false, }, @@ -66,7 +68,7 @@ export function createBraintrustSinks(): InjectedSinks { } span.end(); workflowSpans.delete(info.runId); - workflowSpanExports.delete(info.runId); + workflowTraceContexts.delete(info.runId); } }, callDuringReplay: false, diff --git a/integrations/temporal-js/src/temporal.test.ts b/integrations/temporal-js/src/temporal.test.ts index 04b9c43c0..f8a8732f4 100644 --- a/integrations/temporal-js/src/temporal.test.ts +++ b/integrations/temporal-js/src/temporal.test.ts @@ -2,15 +2,15 @@ import { expect, test, describe } from "vitest"; import { serializeHeaderValue, deserializeHeaderValue, - BRAINTRUST_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_ID_HEADER, + deserializeTraceContext, + serializeTraceContext, + withParentSpanId, } from "./utils"; import { SpanComponentsV3, SpanComponentsV4, SpanObjectTypeV3, -} from "braintrust/util"; +} from "../../../js/util"; import { BraintrustTemporalPlugin, createBraintrustTemporalPlugin, @@ -69,12 +69,29 @@ describe("temporal header utilities", () => { } }); - test("header constants are defined", () => { - expect(BRAINTRUST_SPAN_HEADER).toBe("_braintrust-span"); - expect(BRAINTRUST_WORKFLOW_SPAN_HEADER).toBe("_braintrust-workflow-span"); - expect(BRAINTRUST_WORKFLOW_SPAN_ID_HEADER).toBe( - "_braintrust-workflow-span-id", + test("round-trips W3C trace context through Temporal payloads", () => { + const context = { + traceparent: "00-12345678901234567890123456789012-1234567890123456-01", + tracestate: "vendor=value", + baggage: "braintrust.parent=project_name%3Atest", + }; + expect(deserializeTraceContext(serializeTraceContext(context))).toEqual( + context, + ); + }); + + test("reparents W3C context to the workflow span", () => { + const context = withParentSpanId( + { + traceparent: "00-12345678901234567890123456789012-1234567890123456-01", + baggage: "braintrust.parent=project_name%3Atest", + }, + "abcdefabcdefabcd", ); + expect(context).toEqual({ + traceparent: "00-12345678901234567890123456789012-abcdefabcdefabcd-01", + baggage: "braintrust.parent=project_name%3Atest", + }); }); }); diff --git a/integrations/temporal-js/src/utils.ts b/integrations/temporal-js/src/utils.ts index dc5add88a..51ffcae33 100644 --- a/integrations/temporal-js/src/utils.ts +++ b/integrations/temporal-js/src/utils.ts @@ -1,9 +1,7 @@ import type { Payload } from "@temporalio/common"; +import type { PropagationContext } from "braintrust"; -export const BRAINTRUST_SPAN_HEADER = "_braintrust-span"; -export const BRAINTRUST_WORKFLOW_SPAN_HEADER = "_braintrust-workflow-span"; -export const BRAINTRUST_WORKFLOW_SPAN_ID_HEADER = - "_braintrust-workflow-span-id"; +const TRACE_CONTEXT_HEADERS = ["traceparent", "tracestate", "baggage"] as const; export function serializeHeaderValue(value: string): Payload { return { @@ -27,3 +25,41 @@ export function deserializeHeaderValue( return undefined; } } + +export function serializeTraceContext( + context: PropagationContext, +): Record { + return Object.fromEntries( + TRACE_CONTEXT_HEADERS.flatMap((name) => { + const value = context[name]; + return value ? [[name, serializeHeaderValue(value)]] : []; + }), + ); +} + +export function deserializeTraceContext( + headers: Record | undefined, +): PropagationContext | undefined { + if (!headers) return undefined; + const context = Object.fromEntries( + TRACE_CONTEXT_HEADERS.flatMap((name) => { + const value = deserializeHeaderValue(headers[name]); + return value ? [[name, value]] : []; + }), + ); + return context.traceparent ? context : undefined; +} + +export function withParentSpanId( + context: PropagationContext, + spanId: string, +): PropagationContext | undefined { + const match = context.traceparent.match( + /^([0-9a-f]{2})-([0-9a-f]{32})-[0-9a-f]{16}-([0-9a-f]{2})$/, + ); + if (!match) return undefined; + return { + ...context, + traceparent: `${match[1]}-${match[2]}-${spanId}-${match[3]}`, + }; +} diff --git a/integrations/temporal-js/src/workflow-interceptors.ts b/integrations/temporal-js/src/workflow-interceptors.ts index 914dcc154..b7bc0a2b8 100644 --- a/integrations/temporal-js/src/workflow-interceptors.ts +++ b/integrations/temporal-js/src/workflow-interceptors.ts @@ -12,7 +12,6 @@ import { WorkflowExecuteInput, Next, proxySinks, - workflowInfo, uuid4, } from "@temporalio/workflow"; import type { @@ -21,13 +20,12 @@ import type { StartChildWorkflowExecutionInput, } from "@temporalio/workflow"; import type { Payload } from "@temporalio/common"; +import type { PropagationContext } from "braintrust"; import type { BraintrustSinks } from "./sinks"; import { - BRAINTRUST_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_HEADER, - BRAINTRUST_WORKFLOW_SPAN_ID_HEADER, - serializeHeaderValue, - deserializeHeaderValue, + deserializeTraceContext, + serializeTraceContext, + withParentSpanId, } from "./utils"; const { braintrust } = proxySinks(); @@ -37,7 +35,7 @@ const { braintrust } = proxySinks(); * Created per-workflow by the factory function to avoid global state issues. */ interface WorkflowSpanState { - parentContext: string | undefined; + parentContext: PropagationContext | undefined; spanId: string | undefined; } @@ -49,15 +47,13 @@ class BraintrustWorkflowInboundInterceptor implements WorkflowInboundCallsInterc next: Next, ): Promise { // Extract parent context from headers - const parentContext = input.headers - ? deserializeHeaderValue(input.headers[BRAINTRUST_SPAN_HEADER]) - : undefined; + const parentContext = deserializeTraceContext(input.headers); // Store for the outbound interceptor to forward to activities this.state.parentContext = parentContext; // Generate a deterministic spanId for the workflow span - this.state.spanId = uuid4(); + this.state.spanId = uuid4().replace(/-/g, "").slice(0, 16); // Create workflow span via sink (only called if not replaying) // NOTE: WorkflowInfo is injected automatically by the runtime @@ -78,27 +74,11 @@ class BraintrustWorkflowOutboundInterceptor implements WorkflowOutboundCallsInte constructor(private state: WorkflowSpanState) {} private getHeaders(): Record { - const info = workflowInfo(); - const headers: Record = {}; - - // Pass runId so activity can look up workflow span on same worker - headers[BRAINTRUST_WORKFLOW_SPAN_HEADER] = serializeHeaderValue(info.runId); - - // Pass workflow span ID for cross-worker activities to construct parent - if (this.state.spanId) { - headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER] = serializeHeaderValue( - this.state.spanId, - ); - } - - // Pass client context for cross-worker activities to construct parent - if (this.state.parentContext) { - headers[BRAINTRUST_SPAN_HEADER] = serializeHeaderValue( - this.state.parentContext, - ); - } - - return headers; + const context = + this.state.parentContext && this.state.spanId + ? withParentSpanId(this.state.parentContext, this.state.spanId) + : undefined; + return context ? serializeTraceContext(context) : {}; } scheduleActivity( diff --git a/integrations/temporal-js/tsconfig.json b/integrations/temporal-js/tsconfig.json index 8cfc6d9ae..352d6b9f7 100644 --- a/integrations/temporal-js/tsconfig.json +++ b/integrations/temporal-js/tsconfig.json @@ -2,9 +2,9 @@ "compilerOptions": { "declaration": true, "lib": ["es2022"], - "module": "commonjs", + "module": "esnext", "target": "es2022", - "moduleResolution": "node", + "moduleResolution": "bundler", "strict": true, "noUnusedLocals": true, "esModuleInterop": true, diff --git a/integrations/temporal-js/tsup.config.ts b/integrations/temporal-js/tsup.config.ts index e37c36c4b..6563ce288 100644 --- a/integrations/temporal-js/tsup.config.ts +++ b/integrations/temporal-js/tsup.config.ts @@ -7,7 +7,6 @@ export default defineConfig([ outDir: "dist", external: [ "braintrust", - "braintrust/util", "@braintrust/temporal/workflow-interceptors", "@temporalio/activity", "@temporalio/client", diff --git a/integrations/vercel-ai-sdk/src/adapter.test.ts b/integrations/vercel-ai-sdk/src/adapter.test.ts index 14d9e698e..99fcb5ee7 100644 --- a/integrations/vercel-ai-sdk/src/adapter.test.ts +++ b/integrations/vercel-ai-sdk/src/adapter.test.ts @@ -1,8 +1,15 @@ -import { BraintrustStreamChunk } from "braintrust"; +import { BraintrustStream } from "braintrust"; import { ReadableStream, WritableStream } from "stream/web"; import { expect, test } from "vitest"; import { BraintrustAdapter } from "."; +type BraintrustStreamChunk = + ReturnType extends ReadableStream< + infer Chunk + > + ? Chunk + : never; + test("text callbacks", async () => { const textStream = new ReadableStream({ start(controller) { diff --git a/integrations/vercel-ai-sdk/src/adapter.ts b/integrations/vercel-ai-sdk/src/adapter.ts index 7bb36ffa4..ee1c9d92b 100644 --- a/integrations/vercel-ai-sdk/src/adapter.ts +++ b/integrations/vercel-ai-sdk/src/adapter.ts @@ -1,4 +1,4 @@ -import { BraintrustStream, BraintrustStreamChunk } from "braintrust"; +import { BraintrustStream } from "braintrust"; export interface AIStreamCallbacksAndOptions { onStart?: () => void | Promise; @@ -15,6 +15,13 @@ function formatStreamPart(type: "text" | "data", value: unknown): string { import { ReadableStream, TransformStream } from "stream/web"; +type BraintrustStreamChunk = + ReturnType extends ReadableStream< + infer Chunk + > + ? Chunk + : never; + export type BraintrustStreamOrReadable = | BraintrustStream | ReadableStream @@ -47,16 +54,6 @@ export function toDataStreamResponse( }); } -/** - * @deprecated Use `toDataStreamResponse` instead. - */ -export function toAIStreamResponse( - stream: BraintrustStreamOrReadable, - init?: ResponseInit, -): Response { - return toDataStreamResponse(stream, init); -} - function btStreamToAISDKTransformStream( callbacks?: AIStreamCallbacksAndOptions, ) { diff --git a/js/README.md b/js/README.md index da6da81a6..a350c1423 100644 --- a/js/README.md +++ b/js/README.md @@ -136,7 +136,7 @@ The wrappers are composable and idempotent. They preserve LangSmith behavior, in Disable LangSmith instrumentation in code or through the environment: ```typescript -import { configureInstrumentation } from "braintrust"; +import { configureInstrumentation } from "braintrust/instrumentation"; configureInstrumentation({ integrations: { langsmith: false } }); ``` @@ -153,7 +153,7 @@ When Braintrust LangChain/LangGraph instrumentation is enabled, LangSmith runs s See the [Migrate from v2.x to v3.x guide](https://www.braintrust.dev/docs/reference/sdks/typescript/migrations/v2-to-v3). -In 3.x, browser usage should move to `@braintrust/browser` instead of relying on the legacy `braintrust/browser` path. +For direct browser imports, use `@braintrust/browser`. The package root still selects its browser build through export conditions, but `braintrust/browser` is no longer published. ### Upgrading from 1.x to 2.x diff --git a/js/package.json b/js/package.json index 2f9375376..c62e2ce92 100644 --- a/js/package.json +++ b/js/package.json @@ -46,11 +46,6 @@ "require": "./dist/edge-light.js", "default": "./dist/edge-light.mjs" }, - "./browser": { - "import": "./dist/browser.mjs", - "require": "./dist/browser.js", - "default": "./dist/browser.mjs" - }, "./apply-auto-instrumentation": { "types": "./dist/apply-auto-instrumentation.d.ts", "edge-light": "./dist/apply-auto-instrumentation.browser.mjs", @@ -76,12 +71,6 @@ "module": "./dist/index.mjs", "require": "./dist/index.js" }, - "./util": { - "types": "./util/dist/index.d.ts", - "import": "./util/dist/index.mjs", - "module": "./util/dist/index.mjs", - "require": "./util/dist/index.js" - }, "./instrumentation": { "types": "./dist/instrumentation/index.d.ts", "import": "./dist/instrumentation/index.mjs", @@ -107,10 +96,6 @@ "module": "./dist/auto-instrumentations/bundler/next.mjs", "require": "./dist/auto-instrumentations/bundler/next.cjs" }, - "./webpack-loader": { - "types": "./dist/auto-instrumentations/bundler/webpack-loader.d.ts", - "require": "./dist/auto-instrumentations/bundler/webpack-loader.cjs" - }, "./esbuild": { "types": "./dist/auto-instrumentations/bundler/esbuild.d.ts", "import": "./dist/auto-instrumentations/bundler/esbuild.mjs", @@ -126,7 +111,6 @@ }, "files": [ "dist/**/*", - "util/dist/**/*", "bin/bt", "scripts/bt-helper.js", "scripts/install.js", @@ -138,7 +122,7 @@ "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsup", "check:typings": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", "watch": "tsup --watch", - "clean": "rm -r dist/* && rm -r util/dist/*", + "clean": "rm -r dist/*", "docs": "typedoc --options typedoc.json src/node/index.ts", "test": "vitest run --exclude \"src/wrappers/**/*.test.ts\" --exclude \"src/otel/**/*.test.ts\" --exclude \"smoke/**/*.test.ts\" --exclude \"tests/api-compatibility/**\"", "test:core": "pnpm prune && pnpm test", diff --git a/js/smoke/scenarios/browser-main-package/README.md b/js/smoke/scenarios/browser-main-package/README.md index 1b78d3562..58a6f72fb 100644 --- a/js/smoke/scenarios/browser-main-package/README.md +++ b/js/smoke/scenarios/browser-main-package/README.md @@ -1,6 +1,6 @@ # Browser Main Package Smoke Test -This smoke test verifies that the informational message appears when using the browser build from the main `braintrust` package. +This smoke test verifies the curated browser API exposed by the main `braintrust` package. ## What This Tests @@ -10,11 +10,12 @@ When a user imports from the main `braintrust` package in a browser environment: import * as braintrust from "braintrust"; ``` -The bundler (via the `"browser"` field in package.json) will resolve to the browser build (`dist/browser.mjs`), which should: +The package's browser export condition resolves to `dist/browser.mjs`, which should: -1. Show an informational console message suggesting `@braintrust/browser` for optimal use -2. Provide working browser-safe implementations -3. Not include Node.js modules +1. Provide working browser-safe implementations +2. Expose the supported root API +3. Omit removed legacy and internal exports +4. Not include Node.js modules ## Test Structure @@ -44,21 +45,6 @@ pnpm exec playwright test ## What Gets Verified ✓ Import from main package works in browser -✓ Basic functions are available (init, newId, traceable) -✓ Informational message appears in console -✓ Message mentions "@braintrust/browser" package +✓ Supported functions are available (`init`, `flush`) +✓ Legacy exports such as `newId` and `traceable` are absent ✓ No Node.js module errors - -## Expected Console Output - -When the test runs, you should see: - -``` -Braintrust SDK Browser Build -You are using a browser-compatible build from the main package. -For optimal browser support consider: - npm install @braintrust/browser - import * as braintrust from "@braintrust/browser" -``` - -This message guides users toward the optimized `@braintrust/browser` package while ensuring the main package works correctly in browsers. diff --git a/js/smoke/scenarios/browser-main-package/src/browser-message-test.ts b/js/smoke/scenarios/browser-main-package/src/browser-message-test.ts index dd728e468..92f570a50 100644 --- a/js/smoke/scenarios/browser-main-package/src/browser-message-test.ts +++ b/js/smoke/scenarios/browser-main-package/src/browser-message-test.ts @@ -5,8 +5,8 @@ declare global { consoleMessages: string[]; importSuccessful: boolean; hasInit: boolean; - hasNewId: boolean; - hasTraceable: boolean; + hasFlush: boolean; + removedLegacyExports: boolean; }; } } @@ -22,13 +22,14 @@ console.info = (...args: any[]) => { // Import from main package browser export AFTER setting up console capture // This must be done dynamically to ensure console.info is overridden first -const braintrust = await import("braintrust/browser"); +const braintrust = await import("braintrust"); // Test that imports work const importSuccessful = true; const hasInit = typeof braintrust.init === "function"; -const hasNewId = typeof braintrust.newId === "function"; -const hasTraceable = typeof braintrust.traceable === "function"; +const hasFlush = typeof braintrust.flush === "function"; +const removedLegacyExports = + !("newId" in braintrust) && !("traceable" in braintrust); // Store results window.__btBrowserMessageTest = { @@ -36,8 +37,8 @@ window.__btBrowserMessageTest = { consoleMessages: capturedMessages, importSuccessful, hasInit, - hasNewId, - hasTraceable, + hasFlush, + removedLegacyExports, }; // Display results @@ -48,8 +49,8 @@ if (output) {
  • Import successful: ${importSuccessful ? "✓" : "✗"}
  • Has init function: ${hasInit ? "✓" : "✗"}
  • -
  • Has newId function: ${hasNewId ? "✓" : "✗"}
  • -
  • Has traceable function: ${hasTraceable ? "✓" : "✗"}
  • +
  • Has flush function: ${hasFlush ? "✓" : "✗"}
  • +
  • Legacy exports removed: ${removedLegacyExports ? "✓" : "✗"}
  • Console messages captured: ${capturedMessages.length}

Console Messages:

diff --git a/js/smoke/scenarios/browser-main-package/tests/browser-message.test.ts b/js/smoke/scenarios/browser-main-package/tests/browser-message.test.ts index 8c5d9fb7e..3a48a7f60 100644 --- a/js/smoke/scenarios/browser-main-package/tests/browser-message.test.ts +++ b/js/smoke/scenarios/browser-main-package/tests/browser-message.test.ts @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test"; test.describe("Braintrust Main Package Browser Build", () => { - test("should display informational message when using browser build", async ({ + test("should expose the curated root API in a browser", async ({ page, baseURL, }) => { @@ -55,7 +55,7 @@ test.describe("Braintrust Main Package Browser Build", () => { expect(testResults.completed).toBe(true); expect(testResults.importSuccessful).toBe(true); expect(testResults.hasInit).toBe(true); - expect(testResults.hasNewId).toBe(true); - expect(testResults.hasTraceable).toBe(true); + expect(testResults.hasFlush).toBe(true); + expect(testResults.removedLegacyExports).toBe(true); }); }); diff --git a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs index 848eaef79..a5af1c383 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs +++ b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs @@ -86,7 +86,7 @@ async function testViteDevServerWithNodeEsm() { details: "Vite's dependency pre-bundler cannot handle Nunjucks' use of Object.setPrototypeOf when pre-bundling braintrust/node", recommendation: - "Use 'braintrust/browser' import or configure Vite to exclude Nunjucks from optimization", + "Import 'braintrust' through its browser condition or configure Vite to exclude Nunjucks from optimization", }; } @@ -163,7 +163,7 @@ async function testViteDevServerWithNodeEsm() { "Expected failure: Nunjucks incompatibility with Vite bundler when using braintrust/node. " + "Root cause: Nunjucks uses Object.setPrototypeOf in ways incompatible with Vite's ESM bundler. " + "The Node.js ESM build includes Nunjucks, which causes this error during Vite's dependency pre-bundling. " + - "Recommendation: Use 'braintrust/browser' import or exclude Nunjucks from Vite optimization", + "Recommendation: Import 'braintrust' through its browser condition or exclude Nunjucks from Vite optimization", error: errorStack ? { message: testResult.error, diff --git a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/README.md b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/README.md index 2485bd93a..88af0b7e8 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/README.md +++ b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/README.md @@ -10,7 +10,7 @@ Tests whether the Braintrust SDK (browser build) can be loaded in Vite's dev ser **Hono framework:** Uses Hono for elegant routing instead of raw Worker API. -**Browser build:** Uses `braintrust/browser` import since Cloudflare Workers don't support Node.js APIs. +**Browser build:** Imports `braintrust`; the package's browser condition selects the browser-safe build because Cloudflare Workers don't support Node.js APIs. ## Test Suite diff --git a/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/README.md b/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/README.md index 77b51ac01..de82ebdfb 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/README.md +++ b/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/README.md @@ -10,7 +10,7 @@ Tests Braintrust SDK in a Cloudflare Workers environment with Vite bundling via **Hono framework:** Uses Hono for elegant routing instead of raw Worker API. -**Browser build:** Uses `braintrust/browser` import since Cloudflare Workers don't support Node.js APIs. +**Browser build:** Imports `braintrust`; the package's browser condition selects the browser-safe build because Cloudflare Workers don't support Node.js APIs. **Wrangler dev:** Tests via Wrangler dev server, which uses Vite to build the worker. diff --git a/js/smoke/scenarios/playwright-browser/src/browser-tests.ts b/js/smoke/scenarios/playwright-browser/src/browser-tests.ts index 403f1abc7..706b13cfd 100644 --- a/js/smoke/scenarios/playwright-browser/src/browser-tests.ts +++ b/js/smoke/scenarios/playwright-browser/src/browser-tests.ts @@ -44,15 +44,6 @@ const harness = createBrowserHarness(output); window.__btBrowserSmokeResults = harness.results; async function runAllTestSuites() { - if (!braintrust._exportsForTestingOnly) { - harness.fail( - "runtime", - "preflight", - new Error("_exportsForTestingOnly not available"), - ); - return; - } - harness.log("=== Running test suites ==="); const { passed, failed, xfail } = await runTests({ diff --git a/js/smoke/shared/src/helpers/register.ts b/js/smoke/shared/src/helpers/register.ts index 9b45d0126..9c0cce707 100644 --- a/js/smoke/shared/src/helpers/register.ts +++ b/js/smoke/shared/src/helpers/register.ts @@ -18,7 +18,10 @@ export interface TestContext { backgroundLogger: BackgroundLogger; } -export type TestFn = (braintrust: BraintrustModule) => Promise; +export type TestFn = ((braintrust: BraintrustModule) => Promise) & { + testName: string; + requiresTestHarness?: boolean; +}; type RegisteredTestFn = ( braintrust: BraintrustModule, @@ -35,24 +38,32 @@ type RegisteredTestFn = ( * - Return a partial TestResult -> merged with pass status * - Throw an error -> fail with error details */ -export function register(name: string, fn: RegisteredTestFn): TestFn { +export function register( + name: string, + fn: RegisteredTestFn, + { requiresTestHarness = false }: { requiresTestHarness?: boolean } = {}, +): TestFn { registeredTests.add(name); - return async (braintrust: BraintrustModule): Promise => { - const testing = braintrust._exportsForTestingOnly as { - setInitialTestState: () => void; - simulateLoginForTests: () => Promise | unknown; - simulateLogoutForTests?: () => Promise | unknown; - useTestBackgroundLogger: () => BackgroundLogger; - clearTestBackgroundLogger: () => void; - }; + const test = async (braintrust: BraintrustModule): Promise => { + const testing = braintrust._exportsForTestingOnly as + | { + setInitialTestState: () => void; + simulateLoginForTests: () => Promise | unknown; + simulateLogoutForTests?: () => Promise | unknown; + useTestBackgroundLogger: () => BackgroundLogger; + clearTestBackgroundLogger: () => void; + } + | undefined; - testing.setInitialTestState(); - await testing.simulateLoginForTests(); - const backgroundLogger = testing.useTestBackgroundLogger(); + testing?.setInitialTestState(); + await testing?.simulateLoginForTests(); + const backgroundLogger = testing?.useTestBackgroundLogger(); try { - const result = await fn(braintrust, { backgroundLogger }); + const result = await fn(braintrust, { + backgroundLogger: backgroundLogger!, + }); if (typeof result === "string") { return { status: "pass", name, message: result }; @@ -71,12 +82,15 @@ export function register(name: string, fn: RegisteredTestFn): TestFn { }, }; } finally { - testing.clearTestBackgroundLogger(); - if (typeof testing.simulateLogoutForTests === "function") { + testing?.clearTestBackgroundLogger(); + if (typeof testing?.simulateLogoutForTests === "function") { await testing.simulateLogoutForTests(); } } }; + test.testName = name; + test.requiresTestHarness = requiresTestHarness; + return test; } /** @@ -146,9 +160,21 @@ export async function runTests({ skipCoverage = false, }: RunTestsOptions): Promise { const results: TestResult[] = []; + const testModule: BraintrustModule = { + ...braintrust, + __publicModule: braintrust, + }; for (const test of tests) { - results.push(await test(braintrust)); + if (!testModule._exportsForTestingOnly && test.requiresTestHarness) { + results.push({ + status: "xfail", + name: test.testName, + message: "Requires the private SDK test harness", + }); + } else { + results.push(await test(testModule)); + } } if (!skipCoverage) { @@ -186,7 +212,7 @@ export function expectFailure( predicate: ErrorPredicate, reason: string, ): TestFn { - return async (braintrust: BraintrustModule): Promise => { + const wrapped = async (braintrust: BraintrustModule): Promise => { const result = await test(braintrust); if (result.status === "fail" && result.error) { @@ -200,4 +226,7 @@ export function expectFailure( } return result; }; + wrapped.testName = test.testName; + wrapped.requiresTestHarness = test.requiresTestHarness; + return wrapped; } diff --git a/js/smoke/shared/src/suites/basic-logging.ts b/js/smoke/shared/src/suites/basic-logging.ts index 9bd174022..1a92af9d4 100644 --- a/js/smoke/shared/src/suites/basic-logging.ts +++ b/js/smoke/shared/src/suites/basic-logging.ts @@ -47,6 +47,7 @@ export const testBasicSpanLogging = register( return "Basic span logging test passed"; }, + { requiresTestHarness: true }, ); export const testMultipleSpans = register( @@ -78,6 +79,7 @@ export const testMultipleSpans = register( return `Multiple spans test passed (${events.length} events captured)`; }, + { requiresTestHarness: true }, ); export const testDirectLogging = register( @@ -102,6 +104,7 @@ export const testDirectLogging = register( return "Direct logging test passed"; }, + { requiresTestHarness: true }, ); export const testJSONAttachment = register( @@ -159,6 +162,7 @@ export const testJSONAttachment = register( return "JSONAttachment test passed"; }, + { requiresTestHarness: true }, ); export const testAsyncLocalStorageTraced = register( @@ -221,6 +225,7 @@ export const testAsyncLocalStorageTraced = register( ); } }, + { requiresTestHarness: true }, ); export const testNestedTraced = register( @@ -301,6 +306,7 @@ export const testNestedTraced = register( return "ALS not available in this environment, test skipped"; } }, + { requiresTestHarness: true }, ); export const testCurrentSpan = register( @@ -355,4 +361,5 @@ export const testCurrentSpan = register( throw new Error("currentSpan() returned a span ID that was not logged"); } }, + { requiresTestHarness: true }, ); diff --git a/js/smoke/shared/src/suites/import-verification.ts b/js/smoke/shared/src/suites/import-verification.ts index cf422006b..220ff39b5 100644 --- a/js/smoke/shared/src/suites/import-verification.ts +++ b/js/smoke/shared/src/suites/import-verification.ts @@ -24,6 +24,7 @@ import { register, type TestFn } from "../helpers/register"; * Interface for the Braintrust module based on exports.ts */ export interface BraintrustModule { + __publicModule?: BraintrustModule; // Core logging (REQUIRED) initLogger?: unknown; Logger?: unknown; @@ -87,10 +88,8 @@ export interface BraintrustModule { projects?: unknown; PromptBuilder?: unknown; - // ID Generation (REQUIRED) + // Legacy private smoke-test harness fields IDGenerator?: unknown; - - // Testing (REQUIRED) _exportsForTestingOnly?: unknown; // State management (REQUIRED) @@ -126,13 +125,30 @@ export const testCoreLoggingExports = register( assertDefined(module.startSpan, "startSpan must exist"); assertType(module.startSpan, "function", "startSpan must be a function"); - assertDefined(module.log, "log must exist"); - assertType(module.log, "function", "log must be a function"); - assertDefined(module.flush, "flush must exist"); assertType(module.flush, "function", "flush must be a function"); - return "Core logging exports verified (7 exports)"; + const publicModule = module.__publicModule ?? module; + for (const removed of [ + "default", + "log", + "summarize", + "initExperiment", + "traceable", + "EvalResultWithSummary", + "SpanImpl", + "_exportsForTestingOnly", + "AttachmentReference", + "promptDefinitionSchema", + "braintrustEveInstrumentation", + "braintrustFlueInstrumentation", + ]) { + if (removed in publicModule) { + throw new Error(`${removed} must not be publicly exported`); + } + } + + return "Core logging exports and removals verified"; }, ); @@ -176,12 +192,8 @@ export const testPromptExports = register( export const testExperimentExports = register( "testExperimentExports", async (module) => { - assertDefined(module.initExperiment, "initExperiment must exist"); - assertType( - module.initExperiment, - "function", - "initExperiment must be a function", - ); + assertDefined(module.init, "init must exist"); + assertType(module.init, "function", "init must be a function"); assertDefined(module.Experiment, "Experiment must exist"); assertType( @@ -205,16 +217,6 @@ export const testEvalExports = register("testEvalExports", async (module) => { assertDefined(module.Eval, "Eval must exist"); assertType(module.Eval, "function", "Eval must be a function"); - assertDefined( - module.EvalResultWithSummary, - "EvalResultWithSummary must exist", - ); - assertType( - module.EvalResultWithSummary, - "function", - "EvalResultWithSummary must be a function/class", - ); - assertDefined(module.Reporter, "Reporter must exist"); assertType(module.Reporter, "function", "Reporter must be a function"); @@ -228,7 +230,7 @@ export const testEvalExports = register("testEvalExports", async (module) => { "defaultErrorScoreHandler must be a function", ); - return "Eval exports verified (4 runtime exports)"; + return "Eval exports verified (3 runtime exports)"; }); export const testTracingExports = register( @@ -237,9 +239,6 @@ export const testTracingExports = register( assertDefined(module.traced, "traced must exist"); assertType(module.traced, "function", "traced must be a function"); - assertDefined(module.traceable, "traceable must exist"); - assertType(module.traceable, "function", "traceable must be a function"); - assertDefined(module.wrapTraced, "wrapTraced must exist"); assertType(module.wrapTraced, "function", "wrapTraced must be a function"); @@ -260,7 +259,7 @@ export const testTracingExports = register( "withCurrent must be a function", ); - return "Tracing exports verified (6 exports)"; + return "Tracing exports verified (5 exports)"; }, ); @@ -276,7 +275,6 @@ export const testClientWrapperExports = register( "wrapGoogleGenAI", "wrapOpenRouter", "wrapAISDK", - "wrapMastraAgent", "wrapClaudeAgentSDK", ]; @@ -312,13 +310,7 @@ export const testUtilityExports = register( "Attachment must be a function/class", ); - assertDefined(module.newId, "newId must exist"); - assertType(module.newId, "function", "newId must be a function"); - - assertDefined(module.permalink, "permalink must exist"); - assertType(module.permalink, "function", "permalink must be a function"); - - return "Utility exports verified (4 exports)"; + return "Attachment exports verified (2 exports)"; }, ); @@ -342,20 +334,10 @@ export const testFunctionExports = register( export const testFramework2Exports = register( "testFramework2Exports", async (module) => { - assertDefined(module.Project, "Project must exist"); - assertType(module.Project, "function", "Project must be a function/class"); - assertDefined(module.projects, "projects must exist"); assertType(module.projects, "object", "projects must be an object"); - assertDefined(module.PromptBuilder, "PromptBuilder must exist"); - assertType( - module.PromptBuilder, - "function", - "PromptBuilder must be a function/class", - ); - - return "Framework2 exports verified (3 exports)"; + return "Project facade export verified"; }, ); @@ -369,8 +351,9 @@ export const testIDGeneratorExports = register( "IDGenerator must be a function/class", ); - return "ID generator exports verified (1 export)"; + return "Internal ID generator export verified"; }, + { requiresTestHarness: true }, ); export const testTestingExports = register( @@ -386,8 +369,9 @@ export const testTestingExports = register( "_exportsForTestingOnly must be an object", ); - return "Testing exports verified (1 export)"; + return "Internal testing export verified"; }, + { requiresTestHarness: true }, ); export const testStateManagementExports = register( @@ -415,34 +399,39 @@ export function testBuildResolution( | "workerd" | "node", ): TestFn { - return register("testBuildResolution", async (module) => { - const { buildType: detectedBuild, buildDetails } = detectBuildType(module); - const detectedFormat = detectModuleFormat(); - - if (detectedBuild === "unknown") { - throw new Error( - `Build type is unknown - configureBrowser() or configureNode() was not called. ${buildDetails || ""}`, - ); - } + return register( + "testBuildResolution", + async (module) => { + const { buildType: detectedBuild, buildDetails } = + detectBuildType(module); + const detectedFormat = detectModuleFormat(); + + if (detectedBuild === "unknown") { + throw new Error( + `Build type is unknown - configureBrowser() or configureNode() was not called. ${buildDetails || ""}`, + ); + } - if (expectedBuildType && detectedBuild !== expectedBuildType) { - throw new Error( - `Expected build type "${expectedBuildType}" but detected "${detectedBuild}"`, - ); - } + if (expectedBuildType && detectedBuild !== expectedBuildType) { + throw new Error( + `Expected build type "${expectedBuildType}" but detected "${detectedBuild}"`, + ); + } - const parts: string[] = [`Detected ${detectedBuild} build`]; + const parts: string[] = [`Detected ${detectedBuild} build`]; - if (expectedBuildType) { - parts.push(`(expected: ${expectedBuildType})`); - } + if (expectedBuildType) { + parts.push(`(expected: ${expectedBuildType})`); + } - if (detectedFormat !== "unknown") { - parts.push(`${detectedFormat} format`); - } + if (detectedFormat !== "unknown") { + parts.push(`${detectedFormat} format`); + } - return parts.join(", "); - }); + return parts.join(", "); + }, + { requiresTestHarness: true }, + ); } function detectBuildType(module: BraintrustModule): { diff --git a/js/src/auto-instrumentations/bundler/esbuild.ts b/js/src/auto-instrumentations/bundler/esbuild.ts index dc1443ad8..9e96a2c90 100644 --- a/js/src/auto-instrumentations/bundler/esbuild.ts +++ b/js/src/auto-instrumentations/bundler/esbuild.ts @@ -7,10 +7,3 @@ export function braintrustEsbuildPlugin( ): EsbuildPlugin { return unplugin.esbuild(options); } - -export type EsbuildPluginOptions = BundlerPluginOptions; - -/** - * @deprecated Use {@link braintrustEsbuildPlugin} instead. - */ -export const esbuildPlugin = unplugin.esbuild; diff --git a/js/src/auto-instrumentations/bundler/next.ts b/js/src/auto-instrumentations/bundler/next.ts index 774a4ab98..286be8f99 100644 --- a/js/src/auto-instrumentations/bundler/next.ts +++ b/js/src/auto-instrumentations/bundler/next.ts @@ -1,6 +1,6 @@ import { createRequire } from "node:module"; -import { join } from "node:path"; -import { webpackPlugin } from "./webpack"; +import { dirname, join } from "node:path"; +import { braintrustWebpackPlugin } from "./webpack"; type MaybePromise = T | Promise; type NextConfigFunction = ( @@ -144,7 +144,7 @@ function wrapWebpackConfig( : "server" : "client"; - const plugin = webpackPlugin({ + const plugin = braintrustWebpackPlugin({ browser: runtime === "client" || runtime === "edge", }); @@ -233,11 +233,11 @@ function addBraintrustTurbopackRule( } function getWebpackLoaderPath(): string { - try { - return requireFromProject.resolve("braintrust/webpack-loader"); - } catch { - return "braintrust/webpack-loader"; - } + const packageJsonPath = requireFromProject.resolve("braintrust/package.json"); + return join( + dirname(packageJsonPath), + "dist/auto-instrumentations/bundler/webpack-loader.cjs", + ); } function getNextMajorVersion(): number | undefined { diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index 47562db0d..98361aaea 100644 --- a/js/src/auto-instrumentations/bundler/plugin.ts +++ b/js/src/auto-instrumentations/bundler/plugin.ts @@ -24,17 +24,6 @@ export interface BundlerPluginOptions { * @default false */ browser?: boolean; - - /** - * Marks transformed source as targeting a browser or edge-like environment. - * - * This retains the previous browser-target behavior of the option. Global - * instrumentation hooks are runtime-independent, so no diagnostics-channel - * compatibility shim is injected. - * - * @deprecated Use `browser` instead. - */ - useDiagnosticChannelCompatShim?: boolean; } /** @@ -55,8 +44,7 @@ function getModuleVersion(basedir: string): string | undefined { } export const unplugin = createUnplugin((options = {}) => { - const browser = - options.browser ?? options.useDiagnosticChannelCompatShim ?? false; + const browser = options.browser ?? false; const allInstrumentations = getDefaultAutoInstrumentationConfigs( options.instrumentations, ); diff --git a/js/src/auto-instrumentations/bundler/rollup.ts b/js/src/auto-instrumentations/bundler/rollup.ts index fc93bb614..a27de3589 100644 --- a/js/src/auto-instrumentations/bundler/rollup.ts +++ b/js/src/auto-instrumentations/bundler/rollup.ts @@ -7,10 +7,3 @@ export function braintrustRollupPlugin( ): RollupPlugin | RollupPlugin[] { return unplugin.rollup(options); } - -export type RollupPluginOptions = BundlerPluginOptions; - -/** - * @deprecated Use {@link braintrustRollupPlugin} instead. - */ -export const rollupPlugin = unplugin.rollup; diff --git a/js/src/auto-instrumentations/bundler/vite.ts b/js/src/auto-instrumentations/bundler/vite.ts index 297949eff..51f0e66d2 100644 --- a/js/src/auto-instrumentations/bundler/vite.ts +++ b/js/src/auto-instrumentations/bundler/vite.ts @@ -39,10 +39,3 @@ export function braintrustVitePlugin( ...(Array.isArray(transformPlugin) ? transformPlugin : [transformPlugin]), ]; } - -export type VitePluginOptions = BundlerPluginOptions; - -/** - * @deprecated Use {@link braintrustVitePlugin} instead. - */ -export const vitePlugin = unplugin.vite; diff --git a/js/src/auto-instrumentations/bundler/webpack-loader.ts b/js/src/auto-instrumentations/bundler/webpack-loader.ts index 211cd31a5..80874304e 100644 --- a/js/src/auto-instrumentations/bundler/webpack-loader.ts +++ b/js/src/auto-instrumentations/bundler/webpack-loader.ts @@ -4,21 +4,8 @@ * This is a webpack loader (not a plugin) for compatibility with tools that only support loaders, * such as Next.js Turbopack. * - * Usage in next.config.js / next.config.ts: - * ```javascript - * const nextConfig: NextConfig = { - * turbopack: { - * rules: { - * // Apply the loader to all JS/MJS/CJS files from node_modules. - * // condition: "foreign" restricts the rule to third-party packages only. - * "*.{js,mjs,cjs}": { - * condition: "foreign", - * loaders: [{ loader: require.resolve("braintrust/webpack-loader") }], - * }, - * }, - * }, - * }; - * ``` + * The `braintrust/next` entrypoint resolves this implementation directly when + * configuring Turbopack; it is not a user-facing package entrypoint. */ import { create } from "../orchestrion-js"; @@ -130,7 +117,7 @@ function codeTransformerLoader( modulePath: normalizedModulePath, source: code, format: isModule ? "esm" : "cjs", - browser: options.browser ?? options.useDiagnosticChannelCompatShim ?? false, + browser: options.browser ?? false, }); if (patched !== null) { return callback(null, patched); diff --git a/js/src/auto-instrumentations/bundler/webpack.ts b/js/src/auto-instrumentations/bundler/webpack.ts index b81da8456..0617ee9bc 100644 --- a/js/src/auto-instrumentations/bundler/webpack.ts +++ b/js/src/auto-instrumentations/bundler/webpack.ts @@ -7,10 +7,3 @@ export function braintrustWebpackPlugin( ): WebpackPluginInstance { return unplugin.webpack(options); } - -export type WebpackPluginOptions = BundlerPluginOptions; - -/** - * @deprecated Use {@link braintrustWebpackPlugin} instead. - */ -export const webpackPlugin = unplugin.webpack; diff --git a/js/src/auto-instrumentations/configs/all.ts b/js/src/auto-instrumentations/configs/all.ts index 3b42cb11a..c576eaccf 100644 --- a/js/src/auto-instrumentations/configs/all.ts +++ b/js/src/auto-instrumentations/configs/all.ts @@ -47,11 +47,11 @@ const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] }, { integrations: ["anthropic"], configs: anthropicConfigs }, { - integrations: ["bedrock", "awsBedrock", "awsBedrockRuntime"], + integrations: ["awsBedrockRuntime"], configs: bedrockRuntimeConfigs, }, { - integrations: ["aisdk", "vercel"], + integrations: ["aisdk"], configs: aiSDKConfigs, }, { @@ -70,13 +70,13 @@ const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] integrations: ["cloudflareThink"], configs: cloudflareThinkConfigs, }, - { integrations: ["cursor", "cursorSDK"], configs: cursorSDKConfigs }, + { integrations: ["cursorSDK"], configs: cursorSDKConfigs }, { integrations: ["openAIAgents"], configs: openAIAgentsCoreConfigs, }, { - integrations: ["google", "googleGenAI"], + integrations: ["googleGenAI"], configs: googleGenAIConfigs, }, { diff --git a/js/src/browser/index.ts b/js/src/browser/index.ts index f478444b7..dfbca964f 100644 --- a/js/src/browser/index.ts +++ b/js/src/browser/index.ts @@ -14,4 +14,3 @@ import { configureBrowser } from "./config"; configureBrowser(); export * from "../exports"; -export * as default from "../exports"; diff --git a/js/src/edge-light/index.ts b/js/src/edge-light/index.ts index 4c6ef7bbf..800419f2a 100644 --- a/js/src/edge-light/index.ts +++ b/js/src/edge-light/index.ts @@ -10,4 +10,3 @@ import { configureEdgeLight } from "./config"; configureEdgeLight(); export * from "../exports"; -export * as default from "../exports"; diff --git a/js/src/edge-runtime-bootstrap.test.ts b/js/src/edge-runtime-bootstrap.test.ts index 39f772620..7cfce0fa8 100644 --- a/js/src/edge-runtime-bootstrap.test.ts +++ b/js/src/edge-runtime-bootstrap.test.ts @@ -67,19 +67,19 @@ describe.each([ }); const braintrust = await import(entrypoint); + const { _exportsForTestingOnly } = await import("./logger"); - braintrust._exportsForTestingOnly.setInitialTestState(); - await braintrust._exportsForTestingOnly.simulateLoginForTests(); + _exportsForTestingOnly.setInitialTestState(); + await _exportsForTestingOnly.simulateLoginForTests(); - const backgroundLogger = - braintrust._exportsForTestingOnly.useTestBackgroundLogger(); + const backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); const logger = braintrust.initLogger({ projectId: "test-project-id", projectName, }); const root = logger.startSpan({ name: "root", type: "task" }); - const parent = await root.export(); + const parent = braintrust.extractTraceContextFromHeaders(root.inject())!; const result = await braintrust.traced( async (span: unknown) => { @@ -113,12 +113,12 @@ describe.each([ test("wrapAISDK logs spans in edge runtimes", async () => { const braintrust = await import(entrypoint); + const { _exportsForTestingOnly } = await import("./logger"); - braintrust._exportsForTestingOnly.setInitialTestState(); - await braintrust._exportsForTestingOnly.simulateLoginForTests(); + _exportsForTestingOnly.setInitialTestState(); + await _exportsForTestingOnly.simulateLoginForTests(); - const backgroundLogger = - braintrust._exportsForTestingOnly.useTestBackgroundLogger(); + const backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); braintrust.initLogger({ projectId: "test-project-id", projectName, diff --git a/js/src/exports.ts b/js/src/exports.ts index ffce08390..d08f492e5 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -1,210 +1,79 @@ -import type { z } from "zod/v3"; -import { AttachmentReference as attachmentReferenceSchema } from "./generated_types"; -import type { AttachmentReferenceType } from "./generated_plain_types"; +/** + * The public `braintrust` package surface. + * + * Keep this file as an explicit allowlist. Implementation details should stay + * private; integrations belong on narrow, intentional extension points. + */ export type { - AnyDataset, - AttachmentParams, - BackgroundLoggerOpts, - BaseMetadata, - ChatPrompt, - CompiledPrompt, - CompiledPromptParams, - CompletionPrompt, ContextParentSpanIds, - DatasetRestorePreviewResult, - DatasetRestoreResult, - DatasetSnapshot, - DataSummary, - DatasetSummary, - DefaultMetadataType, - DefaultPromptArgs, - EndSpanArgs, - EvalCase, - ExperimentSummary, - Exportable, - ExternalAttachmentParams, - FullInitDatasetOptions, - FullInitOptions, - FullLoginOptions, - InitDatasetOptions, - InitLoggerOptions, - InitOptions, - LoadPromptOptions, - LoginOptions, - Logs3OverflowInputRow, - Logs3OverflowUpload, - MetricSummary, - ObjectMetadata, + CurrentSpanStore, PropagationContext, - PromptRowWithId, - ScoreSummary, - SerializedBraintrustState, - SetCurrentArg, Span, StartSpanArgs, - WithTransactionId, } from "./logger"; - -export type { - SandboxConfig, - RegisterSandboxOptions, - RegisterSandboxResult, -} from "./sandbox"; - export { Attachment, BaseAttachment, - BRAINTRUST_CURRENT_SPAN_STORE, BraintrustState, - ContextManager, - CurrentSpanStore, - DEFAULT_FETCH_BATCH_SIZE, - DEFAULT_MAX_REQUEST_SIZE, Dataset, - ObjectFetcher, - ERR_PERMALINK, Experiment, ExternalAttachment, - FailedHTTPResponse, JSONAttachment, - LOGS3_OVERFLOW_REFERENCE_TYPE, Logger, - LoginInvalidOrgError, NOOP_SPAN, - NOOP_SPAN_PERMALINK, - NoopSpan, Prompt, ReadonlyAttachment, ReadonlyExperiment, - SpanImpl, - TestBackgroundLogger, - _exportsForTestingOnly, - _internalGetGlobalState, - _internalSetInitialState, - constructLogs3OverflowRequest, currentExperiment, currentLogger, currentSpan, - deepCopyEvent, - deserializePlainStringAsJSON, + configureContextManager, + ContextManager, extractTraceContextFromHeaders, flush, - getContextManager, getPromptVersions, - getSpanParentObject, init, injectTraceContext, initDataset, - initExperiment, initLogger, loadParameters, loadPrompt, - log, logError, login, loginToState, - logs3OverflowUploadSchema, - newId, - permalink, - pickLogs3OverflowObjectIds, - uploadLogs3OverflowPayload, - utf8ByteLength, - renderMessage, - renderPromptParams, setFetch, setMaskingFunction, - spanComponentsToObjectId, startSpan, - summarize, - traceable, traced, updateSpan, withCurrent, - withDataset, - withExperiment, withParent, wrapTraced, - registerOtelFlush, } from "./logger"; export { registerSandbox } from "./sandbox"; -// Internal isomorph layer for platform-specific implementations -import _internalIso from "./isomorph"; -export { _internalIso }; - +export type { TemplateRendererPlugin } from "./template/registry"; +export { registerTemplatePlugin } from "./template/registry"; export { isTemplateFormat, parseTemplateFormat, renderTemplateContent, } from "./template/renderer"; -export type { TemplateFormat } from "./template/registry"; -export type { - TemplateRenderer, - TemplateRendererPlugin, -} from "./template/registry"; -export { - registerTemplatePlugin, - getTemplateRenderer, - templateRegistry, -} from "./template/registry"; - -export type { InvokeFunctionArgs, InvokeReturn } from "./functions/invoke"; export { initFunction, invoke } from "./functions/invoke"; +export { BraintrustStream } from "./functions/stream"; -export type { BraintrustStreamChunk } from "./functions/stream"; -export { - BraintrustStream, - braintrustStreamChunkSchema, - createFinalValuePassThroughStream, - devNullWritableStream, -} from "./functions/stream"; - -export { - IDGenerator, - UUIDGenerator, - OTELIDGenerator, - getIdGenerator, -} from "./id-gen"; - -export { - TRACEPARENT_HEADER, - TRACESTATE_HEADER, - BAGGAGE_HEADER, - BRAINTRUST_PARENT_KEY, -} from "./propagation"; -export type { - ParsedTraceparent, - PropagatedState, - TraceContextCarrier, - TraceContextHeaders, -} from "./propagation"; - -export { - LEGACY_CACHED_HEADER, - X_CACHED_HEADER, - parseCachedHeader, - wrapOpenAI, - wrapOpenAIv4, -} from "./wrappers/oai"; - +export { wrapOpenAI } from "./wrappers/oai"; export { braintrustAISDKTelemetry, wrapAISDK, wrapAgentClass, - BraintrustMiddleware, - wrapAISDKModel, } from "./wrappers/ai-sdk"; -export { braintrustEveInstrumentation } from "./instrumentation"; export { collectAnthropicSession } from "./wrappers/anthropic-session-collector"; export { wrapAnthropic } from "./wrappers/anthropic"; -export { - BraintrustObservabilityExporter, - wrapMastraAgent, -} from "./wrappers/mastra"; -export type { MastraObservabilityExporter } from "./wrappers/mastra"; +export { BraintrustObservabilityExporter } from "./wrappers/mastra"; export { wrapClaudeAgentSDK } from "./wrappers/claude-agent-sdk/claude-agent-sdk"; export { wrapCloudflareThink } from "./wrappers/cloudflare-think"; export { wrapOpenAICodexSDK } from "./wrappers/openai-codex"; @@ -234,37 +103,24 @@ export { } from "./wrappers/langsmith"; export { wrapVitest } from "./wrappers/vitest"; export { initNodeTestSuite } from "./wrappers/node-test"; -export { - BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME, - BraintrustLangChainCallbackHandler, -} from "./wrappers/langchain/callback-handler"; +export { BraintrustLangChainCallbackHandler } from "./wrappers/langchain/callback-handler"; export type { LangChainCallbackHandlerOptions } from "./vendor-sdk-types/langchain"; -export * as graph from "./graph-framework"; - export type { Evaluator, EvalTask, - EvalHooks, - EvalResult, - EvalScorerArgs, EvalScorer, EvalClassifier, - EvaluatorDef, ReporterBody, - SpanContext, } from "./framework"; - export { BaseExperiment, Eval, - EvalResultWithSummary, Reporter, defaultErrorScoreHandler, } from "./framework"; export type { DurableEvalStore } from "./durable-eval"; - export { BatchScorer, BatchTask, @@ -274,61 +130,8 @@ export { } from "./durable-eval"; export { agentAssertionScorer } from "./agent-assertions"; - export { DatasetPipeline } from "./dataset-pipeline"; +export { projects } from "./framework2"; -export type { - CodeOpts, - CreateProjectOpts, - PromptOpts, - ScorerOpts, -} from "./framework2"; - -export { - CodeFunction, - CodePrompt, - Project, - PromptBuilder, - ScorerBuilder, - ToolBuilder, - projects, -} from "./framework2"; - -export { - promptContentsSchema, - promptDefinitionSchema, - promptDefinitionToPromptData, - promptDefinitionWithToolsSchema, - PromptContents, - PromptDefinition, - PromptDefinitionWithTools, -} from "./prompt-schemas"; - -export type { Trace, SpanData, GetThreadOptions } from "./trace"; -export { SpanFetcher, CachedSpanFetcher, LocalTrace } from "./trace"; - -export type { - ParentExperimentIds, - ParentProjectLogIds, - IdField, - InputField, - OtherExperimentLogFields, - ExperimentLogPartialArgs, - ExperimentLogFullArgs, - LogFeedbackFullArgs, - LogCommentFullArgs, - CommentEvent, - DatasetRecord, -} from "../util"; - -export { addAzureBlobHeaders, LazyValue } from "./util"; - -export const AttachmentReference: z.ZodType = - attachmentReferenceSchema; - -export type { EvalParameters } from "./eval-parameters"; - -// Auto-instrumentation configuration -export { configureInstrumentation } from "./instrumentation"; -export { braintrustFlueInstrumentation } from "./instrumentation"; -export type { InstrumentationConfig } from "./instrumentation"; +export type { Trace } from "./trace"; +export { LocalTrace } from "./trace"; diff --git a/js/src/framework.test.ts b/js/src/framework.test.ts index cb905aad7..a61b578c2 100644 --- a/js/src/framework.test.ts +++ b/js/src/framework.test.ts @@ -45,10 +45,8 @@ test("meta (write) is passed to task", async () => { projectName: "proj", evalName: "eval", data: [{ input: 1, metadata }], - task: async (input: number, { meta }) => { - meta({ - foo: "barbar", - }); + task: async (input: number, { metadata }) => { + metadata.foo = "barbar"; return input * 2; }, scores: [], diff --git a/js/src/framework.ts b/js/src/framework.ts index 540b95dc6..9d2bef9d3 100644 --- a/js/src/framework.ts +++ b/js/src/framework.ts @@ -42,7 +42,7 @@ import { startSpan, traced, withCurrent, - withParent, + _internalWithParent, _internalGetGlobalState, } from "./logger"; import type { ProgressReporter } from "./reporters/types"; @@ -131,10 +131,6 @@ export interface EvalHooks< Metadata extends BaseMetadata, Parameters extends EvalParameters, > { - /** - * @deprecated Use `metadata` instead. - */ - meta: (info: Metadata) => void; /** * The metadata object for the current evaluation. You can mutate this object to add or remove metadata. */ @@ -371,37 +367,14 @@ export interface Evaluator< flushBeforeScoring?: boolean; } -export class EvalResultWithSummary< +export interface EvalResultWithSummary< Input, Output, Expected, Metadata extends BaseMetadata = DefaultMetadataType, > { - constructor( - public summary: ExperimentSummary, - public results: EvalResult[], - ) {} - - /** - * @deprecated Use `summary` instead. - */ - toString(): string { - return JSON.stringify(this.summary); - } - - [Symbol.for("nodejs.util.inspect.custom")](): string { - return `EvalResultWithSummary(summary="...", results=[...])`; - } - - toJSON(): { - summary: ExperimentSummary; - results: EvalResult[]; - } { - return { - summary: this.summary, - results: this.results, - }; - } + summary: ExperimentSummary; + results: EvalResult[]; } export type { ReporterBody } from "./reporters/types"; @@ -552,7 +525,7 @@ export function callEvaluatorData< }; } -export type SpanContext = { +type SpanContext = { currentSpan: typeof currentSpan; startSpan: typeof startSpan; withCurrent: typeof withCurrent; @@ -784,15 +757,15 @@ export async function Eval< _initializeSpanContext(); // Better to return this empty object than have an annoying-to-use signature - return new EvalResultWithSummary( - { + return { + summary: { scores: {}, metrics: {}, projectName: "", experimentName: "", }, - [], - ); + results: [], + }; } const progressReporter = options.progress ?? new SimpleProgressReporter(); @@ -840,7 +813,7 @@ export async function Eval< const enableCache = options.enableCache ?? true; let ret; if (options.parent) { - ret = await withParent( + ret = await _internalWithParent( options.parent, () => runEvaluator( @@ -932,9 +905,6 @@ export async function _internalRunEvaluatorTask( ...("metadata" in datum ? datum.metadata : {}), }; const hooks: EvalHooks, EvalParameters> = { - meta(value) { - Object.assign(metadata, value); - }, metadata, expected: "expected" in datum ? datum.expected : undefined, span, @@ -1525,12 +1495,14 @@ async function runEvaluatorInternal( if (!experiment) { // This will almost always be a no-op span, but it means that if the Eval // is run in the context of a different type of span, it will be logged. + const { parent: _ignoredParent, ...spanEvent } = baseEvent; return await traced(callback, { - ...baseEvent, + ...spanEvent, state: evaluator.state, }); } else { - const result = await experiment.traced(callback, baseEvent); + const { parent: _ignoredParent, ...spanEvent } = baseEvent; + const result = await experiment.traced(callback, spanEvent); // Flush logs to provide backpressure and prevent memory accumulation // when maxConcurrency is set. Only flush when pending data exceeds the // byte threshold, avoiding excessive sequential round-trips for small @@ -1675,10 +1647,10 @@ async function runEvaluatorInternal( localScoreAccumulator ?? undefined, ); - return new EvalResultWithSummary( + return { summary, - collectResults ? collectedResults : [], - ); + results: collectResults ? collectedResults : [], + }; } finally { // Clean up disk-based span cache after eval completes and stop caching // Only if it was enabled diff --git a/js/src/framework2.ts b/js/src/framework2.ts index ab4ee52c5..ed1eef15f 100644 --- a/js/src/framework2.ts +++ b/js/src/framework2.ts @@ -41,7 +41,7 @@ const currentFilename = type NameOrId = { name: string } | { id: string }; -export type CreateProjectOpts = NameOrId; +type CreateProjectOpts = NameOrId; class ProjectBuilder { create(opts: CreateProjectOpts) { return new Project(opts); @@ -49,7 +49,7 @@ class ProjectBuilder { } export const projects = new ProjectBuilder(); -export class Project { +class Project { public readonly name?: string; public readonly id?: string; public tools: ToolBuilder; @@ -142,7 +142,7 @@ export class Project { } } -export class ToolBuilder { +class ToolBuilder { private taskCounter = 0; constructor(private readonly project: Project) {} @@ -212,7 +212,7 @@ export class ToolBuilder { } } -export class ScorerBuilder { +class ScorerBuilder { private taskCounter = 0; constructor(private readonly project: Project) {} @@ -335,7 +335,7 @@ type Schema = Partial<{ returns: z.ZodSchema; }>; -export type CodeOpts< +type CodeOpts< Params, Returns, Fn extends GenericFunction, @@ -376,7 +376,7 @@ type ScorerOptsUnion< | CodeOpts>, Returns, Fn> | ScorerPromptOpts; -export type ScorerOpts< +type ScorerOpts< Output, Input, Params, @@ -561,7 +561,7 @@ interface PromptNoTrace { noTrace: boolean; } -export type PromptOpts< +type PromptOpts< HasId extends boolean, HasVersion extends boolean, HasTools extends boolean = true, @@ -575,7 +575,7 @@ export type PromptOpts< (HasNoTrace extends true ? Partial : {}) & PromptDefinition; -export class PromptBuilder { +class PromptBuilder { constructor(private readonly project: Project) {} public create< diff --git a/js/src/functions/invoke.ts b/js/src/functions/invoke.ts index 82fd40de4..1cfe9a5b3 100644 --- a/js/src/functions/invoke.ts +++ b/js/src/functions/invoke.ts @@ -18,11 +18,7 @@ import { z } from "zod/v3"; /** * Arguments for the `invoke` function. */ -export interface InvokeFunctionArgs< - Input, - Output, - Stream extends boolean = false, -> { +interface InvokeFunctionArgs { // These parameters are duplicated from FunctionId, so that we can document them. /** @@ -136,7 +132,7 @@ export interface InvokeFunctionArgs< * if `stream` is true, otherwise returns the output of the function using the Zod schema's * type if present. */ -export type InvokeReturn = Stream extends true +type InvokeReturn = Stream extends true ? BraintrustStream : Output; diff --git a/js/src/functions/stream.ts b/js/src/functions/stream.ts index 392e6b337..901891119 100644 --- a/js/src/functions/stream.ts +++ b/js/src/functions/stream.ts @@ -30,41 +30,40 @@ export type BraintrustStreamChunk = | { type: "start"; data: string } | { type: "done"; data: string }; -export const braintrustStreamChunkSchema: z.ZodType = - z.union([ - z.object({ - type: z.literal("text_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("reasoning_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("json_delta"), - data: z.string(), - }), - z.object({ - type: z.literal("error"), - data: z.string(), - }), - z.object({ - type: z.literal("console"), - data: sseConsoleEventDataSchema, - }), - z.object({ - type: z.literal("progress"), - data: sseProgressEventDataSchema, - }), - z.object({ - type: z.literal("start"), - data: z.string(), - }), - z.object({ - type: z.literal("done"), - data: z.string(), - }), - ]); +const braintrustStreamChunkSchema: z.ZodType = z.union([ + z.object({ + type: z.literal("text_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("reasoning_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("json_delta"), + data: z.string(), + }), + z.object({ + type: z.literal("error"), + data: z.string(), + }), + z.object({ + type: z.literal("console"), + data: sseConsoleEventDataSchema, + }), + z.object({ + type: z.literal("progress"), + data: sseProgressEventDataSchema, + }), + z.object({ + type: z.literal("start"), + data: z.string(), + }), + z.object({ + type: z.literal("done"), + data: z.string(), + }), +]); /** * A Braintrust stream. This is a wrapper around a ReadableStream of `BraintrustStreamChunk`, diff --git a/js/src/graph-framework.ts b/js/src/graph-framework.ts deleted file mode 100644 index 5d6766da6..000000000 --- a/js/src/graph-framework.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { newId, Prompt } from "./logger"; -import type { - FunctionIdType as FunctionId, - GraphDataType as GraphData, - GraphNodeType as GraphNode, - GraphEdgeType as GraphEdge, - PromptBlockDataType as PromptBlockData, -} from "./generated_plain_types"; - -export interface BuildContext { - getFunctionId(functionObj: unknown): Promise; -} - -// Base interface for all node types -export interface Node { - readonly id: string; - __type: "node"; - build(context: BuildContext): Promise; - addDependency(dependency: Dependency): void; -} - -// type CallArgs = ProxyVariable | Node | Record; - -export type NodeLike = Node | Prompt | ProxyVariable; - -export type LazyGraphNode = { - type: "lazy"; - id: string; -}; - -// Graph builder class to convert functional chains to GraphData -export class GraphBuilder { - private nodes = new Map(); - private edges: Record = {}; - - private nodeLikeNodes = new Map(); // Maps node-like objects, like prompts, to their nodes - - // Special nodes - public readonly IN: InputNode; - public readonly OUT: OutputNode; - - constructor() { - // Create input and output nodes - this.IN = this.createInputNode(); - this.OUT = this.createOutputNode(); - } - - // Create the final GraphData object - public async build(context: BuildContext): Promise { - const nodes = await Promise.all( - Array.from(this.nodes.values()).map(async (node) => [ - node.id, - await node.build(context), - ]), - ); - - return { - type: "graph", - nodes: Object.fromEntries(nodes), // XXX Need to resolve the lazy nodes - edges: this.edges, - }; - } - - public addEdge({ - source, - sourceVar, - target, - targetVar, - expr, - purpose, - }: { - source: NodeLike; - sourceVar?: string; - target: NodeLike; - targetVar?: string; - expr?: string; - purpose: GraphEdge["purpose"]; - }) { - const [sourceNode, sourcePath] = this.resolveNode(source); - if (sourcePath.length > 0) { - // XXX Maybe we can remove these paths? - throw new Error("Source path must be empty"); - } - const [targetNode, targetPath] = this.resolveNode(target); - if (targetPath.length > 0) { - throw new Error("Target path must be empty"); - } - const id = this.generateId(); - - sourceVar = sourceVar ?? "output"; - targetVar = - targetVar ?? (purpose === "data" ? "input" : this.generateId("control")); - - // Make sure this variable name doesn't already exist as a target variable - for (const edge of Object.values(this.edges)) { - if ( - edge.target.node === targetNode.id && - edge.target.variable === targetVar - ) { - throw new Error( - `Variable name ${targetVar} already set on ${targetNode.id}`, - ); - } - } - - this.edges[id] = { - source: { node: sourceNode.id, variable: sourceVar }, - target: { node: targetNode.id, variable: targetVar }, - purpose, - }; - } - - public resolveNode(node: NodeLike): [Node, string[]] { - if (node instanceof Prompt) { - const cached = this.nodeLikeNodes.get(node); - if (cached) { - return [cached, []]; - } - const promptNode = this.createPromptNode(node); - this.nodeLikeNodes.set(node, promptNode); - return [promptNode, []]; - } else if (isProxyVariable(node)) { - return proxyVariableToNode(node); - } else { - return [node, []]; - } - } - - // Create a literal node - public literal(value: T): LiteralNode { - const preview = ( - typeof value === "string" ? value : JSON.stringify(value) - ).slice(0, 16); - const id = this.generateId(`literal-${preview}`); - const literalNode = new LiteralNode(this, id, value); - this.nodes.set(id, literalNode); - return literalNode; - } - - public gate(options: { condition: string }): GateNode { - const id = this.generateId("gate"); - const gateNode = new GateNode(this, id, options.condition); - this.nodes.set(id, gateNode); - return gateNode; - } - - public aggregator(): AggregatorNode { - const id = this.generateId("aggregator"); - const aggregatorNode = new AggregatorNode(this, id); - this.nodes.set(id, aggregatorNode); - return aggregatorNode; - } - - public promptTemplate(options: { - prompt: PromptBlockData; - }): PromptTemplateNode { - const id = this.generateId("prompt-template"); - const promptTemplateNode = new PromptTemplateNode(this, id, options.prompt); - this.nodes.set(id, promptTemplateNode); - return promptTemplateNode; - } - - // public call(node: NodeLike, input: CallArgs): Node { - // const [resolvedNode, path] = this.resolveNode(node); - // if (resolvedNode instanceof SingleInputNode) { - // return resolvedNode.call(input, path); - // } else { - // throw new Error("Node must be a SingleInputNode"); - // } - // } - - // Helper to generate node IDs - private generateId(name?: string): string { - const uuid = newId(); - if (name) { - return `${name}-${uuid.slice(0, 8)}`; - } else { - return uuid; - } - } - - // Create an input node - private createInputNode(): InputNode { - const id = this.generateId("input"); - const inputNode = new InputNode(this, id); - this.nodes.set(id, inputNode); - return inputNode; - } - - // Create an output node - private createOutputNode(): OutputNode { - const id = this.generateId("output"); - const outputNode = new OutputNode(this, id); - this.nodes.set(id, outputNode); - return outputNode; - } - - // Create a prompt node from a CodePrompt - private createPromptNode(prompt: Prompt): PromptNode { - const id = this.generateId(`prompt-${prompt.slug}`); - - const promptNode = new PromptNode(this, id, prompt); - this.nodes.set(id, promptNode); - return promptNode; - } -} - -export type ProxyVariable = { - [key: string]: ProxyVariable; -}; - -function isProxyVariable(node: unknown): node is ProxyVariable { - return ( - typeof node === "object" && - node !== null && - "__type" in node && - // @ts-ignore - node.__type === "proxy-variable" - ); -} - -function proxyVariableToNode(proxy: ProxyVariable): [Node, string[]] { - // @ts-ignore - return [proxy.__node, proxy.__path]; -} - -// // Create a proxy handler that captures property access paths -// function createVariableProxy({ -// path, -// node, -// }: { -// path: string[]; -// node: Node; -// }): ProxyVariable { -// // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -// return new Proxy({} as ProxyVariable, { -// get(target, prop) { -// if (typeof prop === "string") { -// if (prop === "__type") { -// return "proxy-variable"; -// } else if (prop === "__node") { -// return node; -// } else if (prop === "__path") { -// return path; -// } - -// const newPath = [...path, prop]; - -// // Return a variable reference for terminal properties -// // or a new proxy for further chaining -// return createVariableProxy({ path: newPath, node }); -// } -// return undefined; -// }, -// has(target, prop) { -// return typeof prop === "string"; -// }, -// }); -// } - -// Type for transform functions -export type TransformFn = (input: ProxyVariable) => Node; - -interface Dependency { - node: Node; - sourceVar?: string; - targetVar?: string; - expr?: string; -} - -// Base Node class for common functionality -abstract class BaseNode implements Node { - public readonly __type = "node"; - public dependencies: Dependency[] = []; - - constructor( - protected graph: GraphBuilder, - public readonly id: string, - ) {} - - public addDependency(dependency: Dependency) { - this.dependencies.push(dependency); - } - - abstract build(context: BuildContext): Promise; -} - -// abstract class SingleOutputNode extends BaseNode { -// // Connect this node to another node -// public then(...args: Array): Node { -// // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -// const callableThis = this as unknown as Node; -// let lastNode: Node = callableThis; - -// // Connect each arg to this node -// for (const arg of args) { -// // Handle different types of arguments -// if (typeof arg === "function") { -// // This function is expected to take dependencies as needed on the argsProxy. -// const argsProxy = createVariableProxy({ path: [], node: callableThis }); -// const result = arg(argsProxy); -// lastNode = result; -// } else { -// const [node, path] = this.graph.resolveNode(arg); -// lastNode = node; -// node.addDependency({ node: callableThis, expr: escapePath(path) }); -// } -// } - -// return lastNode; -// } -// } - -// abstract class SingleInputNode extends BaseNode { -// public call(input: CallArgs, path?: string[]): Node { -// if (isProxyVariable(input)) { -// const [sourceNode, sourcePath] = proxyVariableToNode(input); -// this.addDependency({ node: sourceNode, expr: escapePath(sourcePath) }); -// } else if (isNode(input)) { -// this.addDependency({ -// node: input, -// expr: path ? escapePath(path) : undefined, -// }); -// } else { -// for (const [targetVar, source] of Object.entries(input)) { -// const [sourceNode, sourcePath] = this.graph.resolveNode(source); -// this.addDependency({ -// node: sourceNode, -// expr: sourcePath ? escapePath(sourcePath) : undefined, -// targetVar, -// }); -// } -// } -// return this; -// } -// } - -// function isNode(node: unknown): node is Node { -// return ( -// typeof node === "object" && -// node !== null && -// "__type" in node && -// // @ts-ignore -// node.__type === "node" -// ); -// } - -// Input node (entry point to the graph) -export class InputNode extends BaseNode implements Node { - constructor(graph: GraphBuilder, id: string) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "input", - description: "Input to the graph", - }; - } -} - -// Output node (exit point from the graph) -export class OutputNode extends BaseNode implements Node { - constructor(graph: GraphBuilder, id: string) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "output", - description: "Output of the graph", - }; - } -} - -// Prompt node (wrapper for CodePrompt) -export class PromptNode extends BaseNode implements Node { - constructor( - graph: GraphBuilder, - id: string, - private prompt: Prompt, - ) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "function", - function: await context.getFunctionId(this.prompt), - }; - } -} - -// Gate node for conditional branching -export class GateNode extends BaseNode implements Node { - constructor( - graph: GraphBuilder, - id: string, - private condition: string, - ) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "gate", - description: "Conditional gate", - condition: this.condition, - }; - } -} - -export class AggregatorNode extends BaseNode implements Node { - constructor(graph: GraphBuilder, id: string) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "aggregator", - description: "Aggregator", - }; - } -} - -export class PromptTemplateNode extends BaseNode implements Node { - constructor( - graph: GraphBuilder, - id: string, - private prompt: PromptBlockData, - ) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "prompt_template", - prompt: this.prompt, - }; - } -} - -// Literal node for constant values -export class LiteralNode extends BaseNode implements Node { - constructor( - graph: GraphBuilder, - id: string, - private value: T, - ) { - super(graph, id); - } - - public async build(context: BuildContext): Promise { - return { - type: "literal", - value: this.value, - }; - } -} - -// Create a graph instance with IN and OUT nodes -export function createGraph(): GraphBuilder { - const graphBuilder = new GraphBuilder(); - return graphBuilder; -} - -// XXX write tests -export function escapePath(parts: string[]): string | undefined { - if (parts.length === 0) { - return undefined; - } - return parts - .map((part) => { - if (/[^\w-]/.test(part)) { - // Escape special characters properly - const escaped = part.replace(/["\\]/g, "\\$&"); - return `"${escaped}"`; - } - return part; - }) - .join("."); -} - -export function unescapePath(path: string): string[] { - const regex = /"((?:\\["\\]|[^"\\])*)"|([^\.]+)/g; - const matches = path.match(regex); - return matches - ? matches.map((match) => { - if (match.startsWith('"')) { - // Remove surrounding quotes and unescape special characters - return match.slice(1, -1).replace(/\\(["\\])/g, "$1"); - } - return match; - }) - : []; -} - -// Export the graph constructor -export default { createGraph }; diff --git a/js/src/id-gen.ts b/js/src/id-gen.ts index 67925916a..d95e1a0bc 100644 --- a/js/src/id-gen.ts +++ b/js/src/id-gen.ts @@ -136,15 +136,10 @@ export function resolveUseLegacyUuidIds(): boolean { * This eliminates global state and makes tests parallelizable. * Each caller gets their own generator instance. * - * Honors an explicitly-installed `globalThis.BRAINTRUST_ID_GENERATOR` (e.g. set - * by `@braintrust/otel`'s `setupOtelCompat()`). Otherwise it defaults to - * OpenTelemetry-compatible hex IDs, falling back to legacy UUID IDs when - * BRAINTRUST_LEGACY_IDS is set. + * Defaults to OpenTelemetry-compatible hex IDs, falling back to legacy UUID + * IDs when BRAINTRUST_LEGACY_IDS is set. */ export function getIdGenerator(): IDGenerator { - if (globalThis.BRAINTRUST_ID_GENERATOR !== undefined) { - return new globalThis.BRAINTRUST_ID_GENERATOR(); - } return resolveUseLegacyUuidIds() ? new UUIDGenerator() : new OTELIDGenerator(); diff --git a/js/src/instrumentation/config.ts b/js/src/instrumentation/config.ts index e8df49037..edcbe083e 100644 --- a/js/src/instrumentation/config.ts +++ b/js/src/instrumentation/config.ts @@ -1,16 +1,13 @@ export interface InstrumentationIntegrationsConfig { openai?: boolean; anthropic?: boolean; - vercel?: boolean; aisdk?: boolean; - google?: boolean; googleGenAI?: boolean; googleADK?: boolean; huggingface?: boolean; claudeAgentSDK?: boolean; cloudflareAIChat?: boolean; cloudflareThink?: boolean; - cursor?: boolean; cursorSDK?: boolean; mastra?: boolean; openAIAgents?: boolean; @@ -20,8 +17,6 @@ export interface InstrumentationIntegrationsConfig { ollama?: boolean; cohere?: boolean; groq?: boolean; - bedrock?: boolean; - awsBedrock?: boolean; awsBedrockRuntime?: boolean; genkit?: boolean; gitHubCopilot?: boolean; @@ -69,14 +64,14 @@ const envIntegrationAliases: Record< aisdk: "aisdk", "ai-sdk": "aisdk", "vercel-ai": "aisdk", - vercel: "vercel", + vercel: "aisdk", claudeagentsdk: "claudeAgentSDK", "claude-agent-sdk": "claudeAgentSDK", cloudflareaichat: "cloudflareAIChat", "cloudflare-ai-chat": "cloudflareAIChat", "@cloudflare/ai-chat": "cloudflareAIChat", cloudflarethink: "cloudflareThink", - cursor: "cursor", + cursor: "cursorSDK", "cursor-sdk": "cursorSDK", cursorsdk: "cursorSDK", mastra: "mastra", @@ -84,7 +79,7 @@ const envIntegrationAliases: Record< openaiagents: "openAIAgents", "openai-agents-core": "openAIAgents", openaiagentscore: "openAIAgents", - google: "google", + google: "googleGenAI", "google-genai": "googleGenAI", googlegenai: "googleGenAI", huggingface: "huggingface", @@ -100,9 +95,9 @@ const envIntegrationAliases: Record< cohere: "cohere", groq: "groq", "groq-sdk": "groq", - bedrock: "bedrock", - "aws-bedrock": "awsBedrock", - awsbedrock: "awsBedrock", + bedrock: "awsBedrockRuntime", + "aws-bedrock": "awsBedrockRuntime", + awsbedrock: "awsBedrockRuntime", "aws-bedrock-runtime": "awsBedrockRuntime", awsbedrockruntime: "awsBedrockRuntime", "@aws-sdk/client-bedrock-runtime": "awsBedrockRuntime", @@ -129,16 +124,13 @@ export function getDefaultInstrumentationIntegrations(): Record< openai: true, openaiCodexSDK: true, anthropic: true, - vercel: true, aisdk: true, - google: true, googleGenAI: true, googleADK: true, huggingface: true, claudeAgentSDK: true, cloudflareAIChat: true, cloudflareThink: true, - cursor: true, cursorSDK: true, mastra: true, openAIAgents: true, @@ -148,8 +140,6 @@ export function getDefaultInstrumentationIntegrations(): Record< ollama: true, cohere: true, groq: true, - bedrock: true, - awsBedrock: true, awsBedrockRuntime: true, genkit: true, gitHubCopilot: true, diff --git a/js/src/instrumentation/core/channel-tracing.ts b/js/src/instrumentation/core/channel-tracing.ts index 1f3ff47d0..7d69fb12b 100644 --- a/js/src/instrumentation/core/channel-tracing.ts +++ b/js/src/instrumentation/core/channel-tracing.ts @@ -1,11 +1,7 @@ import { debugLogger } from "../../debug-logger"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; -import { - _internalGetGlobalState, - BRAINTRUST_CURRENT_SPAN_STORE, - startSpan, -} from "../../logger"; -import type { CurrentSpanStore, Span } from "../../logger"; +import { _internalGetGlobalState, startSpan } from "../../logger"; +import type { Span } from "../../logger"; import { withSpanInstrumentationName, type SpanInstrumentationName, @@ -316,13 +312,7 @@ function bindCurrentSpanStoreToStart< const state = _internalGetGlobalState(); const startChannel = tracingChannel.start; const contextManager = state?.contextManager; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!currentSpanStore || !startChannel) { return; diff --git a/js/src/instrumentation/index.test.ts b/js/src/instrumentation/index.test.ts index fd5442745..b51519344 100644 --- a/js/src/instrumentation/index.test.ts +++ b/js/src/instrumentation/index.test.ts @@ -8,6 +8,7 @@ describe("instrumentation public API", () => { "braintrustEveInstrumentation", "braintrustFlueInstrumentation", "configureInstrumentation", + "registerOtelFlush", ]); }); }); diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index e148f2dc5..67853cce8 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -15,3 +15,6 @@ export { braintrustEveInstrumentation } from "./providers/eve-instrumentation"; // Configuration API export { configureInstrumentation } from "./registry"; export type { InstrumentationConfig } from "./registry"; + +// Integration hook used by @braintrust/otel so core flushes also drain OTEL. +export { registerOtelFlush } from "../logger"; diff --git a/js/src/instrumentation/instrumentation-consumers.test.ts b/js/src/instrumentation/instrumentation-consumers.test.ts index ce4837bb5..9499bff31 100644 --- a/js/src/instrumentation/instrumentation-consumers.test.ts +++ b/js/src/instrumentation/instrumentation-consumers.test.ts @@ -128,7 +128,7 @@ const integrationCases: Array< > = [ ["aisdk", [registrations.aiSDK]], ["anthropic", [registrations.anthropic]], - ["bedrock", [registrations.bedrockRuntime]], + ["awsBedrockRuntime", [registrations.bedrockRuntime]], ["claudeAgentSDK", [registrations.claudeAgentSDK]], ["cloudflareAgents", [registrations.cloudflareAgents]], ["cloudflareAIChat", [registrations.cloudflareAIChat]], @@ -187,15 +187,8 @@ describe("registerInstrumentationConsumers", () => { }, ); - it.each([ - ["vercel", registrations.aiSDK], - ["google", registrations.googleGenAI], - ["cursor", registrations.cursorSDK], - ["awsBedrock", registrations.bedrockRuntime], - ["awsBedrockRuntime", registrations.bedrockRuntime], - ["langgraph", registrations.langChain], - ] as const)( - "honors the legacy or related %s disable flag", + it.each([["langgraph", registrations.langChain]] as const)( + "honors the related %s disable flag", (integration, register) => { registerInstrumentationConsumers({ integrations: { [integration]: false }, diff --git a/js/src/instrumentation/instrumentation-consumers.ts b/js/src/instrumentation/instrumentation-consumers.ts index ed32522a9..57d01b43d 100644 --- a/js/src/instrumentation/instrumentation-consumers.ts +++ b/js/src/instrumentation/instrumentation-consumers.ts @@ -46,8 +46,7 @@ export function registerInstrumentationConsumers( registerAnthropicInstrumentation(); } - // Support both 'aisdk' and legacy 'vercel' config keys - if (integrations.aisdk !== false && integrations.vercel !== false) { + if (integrations.aisdk !== false) { registerAISDKInstrumentation(); } @@ -59,7 +58,7 @@ export function registerInstrumentationConsumers( registerCloudflareThinkInstrumentation(); } - if (integrations.cursorSDK !== false && integrations.cursor !== false) { + if (integrations.cursorSDK !== false) { registerCursorSDKInstrumentation(); } @@ -67,8 +66,7 @@ export function registerInstrumentationConsumers( registerOpenAIAgentsInstrumentation(); } - // Support both 'googleGenAI' and legacy 'google' config keys - if (integrations.googleGenAI !== false && integrations.google !== false) { + if (integrations.googleGenAI !== false) { registerGoogleGenAIInstrumentation(); } @@ -109,11 +107,7 @@ export function registerInstrumentationConsumers( registerGroqInstrumentation(); } - if ( - integrations.bedrock !== false && - integrations.awsBedrock !== false && - integrations.awsBedrockRuntime !== false - ) { + if (integrations.awsBedrockRuntime !== false) { registerBedrockRuntimeInstrumentation(); } diff --git a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts index 93265e4a3..1394ac919 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts @@ -32,18 +32,22 @@ vi.mock("../core/stream-patcher", () => ({ import { registerClaudeAgentSDKInstrumentation } from "./claude-agent-sdk-instrumentation"; import iso from "../../isomorph"; -import { startSpan } from "../../logger"; +import { _internalStartSpan as startSpan } from "../../logger"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; // Mock the logger module -vi.mock("../../logger", () => ({ - startSpan: vi.fn(() => ({ +vi.mock("../../logger", () => { + const startSpan = vi.fn(() => ({ log: vi.fn(), end: vi.fn(), export: vi.fn(() => Promise.resolve({})), - })), -})); + })); + return { + startSpan, + _internalStartSpan: startSpan, + }; +}); // Mock utility modules vi.mock("../../../util/index", () => ({ diff --git a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts index 9944ebe60..32b5ab345 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts @@ -1,7 +1,7 @@ import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { IsoChannelHandlers } from "../../isomorph"; -import { startSpan as startBaseSpan } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts index 1a1c7db88..2ba99e8c9 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts @@ -1,4 +1,4 @@ -import { startSpan as startBaseSpan, withCurrent } from "../../logger"; +import { _internalStartSpan as startBaseSpan, withCurrent } from "../../logger"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts index 860ab0b18..f0a0c3a72 100644 --- a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts @@ -1,25 +1,19 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const { - mockInternalGetGlobalState, - mockStartSpan, - mockWithCurrent, - mockCurrentSpanStoreSymbol, -} = vi.hoisted(() => ({ - mockCurrentSpanStoreSymbol: Symbol.for("braintrust.currentSpanStore"), - mockInternalGetGlobalState: vi.fn(() => undefined), - mockStartSpan: vi.fn(), - mockWithCurrent: vi.fn((_span: unknown, callback: () => unknown) => - callback(), - ), -})); +const { mockInternalGetGlobalState, mockStartSpan, mockWithCurrent } = + vi.hoisted(() => ({ + mockInternalGetGlobalState: vi.fn(() => undefined), + mockStartSpan: vi.fn(), + mockWithCurrent: vi.fn((_span: unknown, callback: () => unknown) => + callback(), + ), + })); vi.mock("../../isomorph", () => ({ default: { newTracingChannel: vi.fn() }, })); vi.mock("../../logger", () => ({ - BRAINTRUST_CURRENT_SPAN_STORE: mockCurrentSpanStoreSymbol, _internalGetGlobalState: () => mockInternalGetGlobalState(), startSpan: (...args: unknown[]) => (mockStartSpan as any)(...args), withCurrent: (...args: unknown[]) => (mockWithCurrent as any)(...args), diff --git a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts index a75ad3ea6..2258ea666 100644 --- a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts @@ -1,12 +1,11 @@ import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { - BRAINTRUST_CURRENT_SPAN_STORE, _internalGetGlobalState, startSpan as startBaseSpan, withCurrent, } from "../../logger"; -import type { CurrentSpanStore, Span } from "../../logger"; +import type { Span } from "../../logger"; import { debugLogger } from "../../debug-logger"; import { INSTRUMENTATION_NAMES, @@ -134,13 +133,7 @@ class CloudflareAIChatInstrumentationConsumer { const globalState = _internalGetGlobalState(); const contextManager = globalState?.contextManager; const startChannel = tracingChannel.start; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!startChannel || !currentSpanStore || !contextManager) { return; diff --git a/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts index 66196becf..17751542a 100644 --- a/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts +++ b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts @@ -1,11 +1,7 @@ import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; -import { - BRAINTRUST_CURRENT_SPAN_STORE, - _internalGetGlobalState, - startSpan, -} from "../../logger"; -import type { CurrentSpanStore, Span } from "../../logger"; +import { _internalGetGlobalState, startSpan } from "../../logger"; +import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -63,13 +59,7 @@ class CloudflareThinkInstrumentationConsumer { const states = new WeakMap(); const state = _internalGetGlobalState(); const contextManager = state?.contextManager; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); const ensureState = ( event: ChannelMessage, diff --git a/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts index c30169c35..39d3582a6 100644 --- a/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts @@ -12,6 +12,7 @@ vi.mock("../../isomorph", () => ({ vi.mock("../../logger", () => ({ startSpan: (...args: unknown[]) => mockStartSpan(...args), + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), })); import iso from "../../isomorph"; diff --git a/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts b/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts index 41118be17..957d8a997 100644 --- a/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts @@ -2,7 +2,7 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; -import { startSpan as startBaseSpan } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/eve-instrumentation.test.ts b/js/src/instrumentation/providers/eve-instrumentation.test.ts index 9e1a6bc28..48e6b5bff 100644 --- a/js/src/instrumentation/providers/eve-instrumentation.test.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.test.ts @@ -93,9 +93,9 @@ describe("braintrustEveInstrumentation", () => { ]); }); - it("exports only the Eve instrumentation provider API", () => { - expect(braintrustExports.braintrustEveInstrumentation).toBe( - braintrustEveInstrumentation, + it("exports the Eve provider only from the instrumentation entrypoint", () => { + expect(braintrustExports).not.toHaveProperty( + "braintrustEveInstrumentation", ); expect(instrumentationExports.braintrustEveInstrumentation).toBe( braintrustEveInstrumentation, diff --git a/js/src/instrumentation/providers/flue-instrumentation.test.ts b/js/src/instrumentation/providers/flue-instrumentation.test.ts index ae6fbec8b..f1c05a12c 100644 --- a/js/src/instrumentation/providers/flue-instrumentation.test.ts +++ b/js/src/instrumentation/providers/flue-instrumentation.test.ts @@ -3,17 +3,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { mockDebugLog, mockCurrentParentSpan, - mockCurrentSpanStoreSymbol, mockCurrentSpanStore, mockFlush, mockStartSpan, } = vi.hoisted(() => { const currentParentSpan = { current: undefined as any }; - const currentSpanStoreSymbol = Symbol.for("braintrust.currentSpanStore"); return { mockDebugLog: vi.fn(), mockCurrentParentSpan: currentParentSpan, - mockCurrentSpanStoreSymbol: currentSpanStoreSymbol, mockCurrentSpanStore: { getStore: vi.fn(() => currentParentSpan.current), run: vi.fn((span: unknown, callback: () => unknown) => { @@ -38,12 +35,11 @@ vi.mock("../../debug-logger", () => ({ })); vi.mock("../../logger", () => ({ - BRAINTRUST_CURRENT_SPAN_STORE: mockCurrentSpanStoreSymbol, NOOP_SPAN: {}, flush: (...args: unknown[]) => mockFlush(...args), _internalGetGlobalState: () => ({ contextManager: { - [mockCurrentSpanStoreSymbol]: mockCurrentSpanStore, + getCurrentSpanStore: () => mockCurrentSpanStore, wrapSpanForStore: (span: unknown) => span, }, idGenerator: { @@ -53,6 +49,7 @@ vi.mock("../../logger", () => ({ }, }), startSpan: (...args: unknown[]) => mockStartSpan(...args), + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), withCurrent: (span: unknown, callback: () => unknown) => { const previous = mockCurrentParentSpan.current; mockCurrentParentSpan.current = span; diff --git a/js/src/instrumentation/providers/flue-instrumentation.ts b/js/src/instrumentation/providers/flue-instrumentation.ts index 1426afde6..51fa32881 100644 --- a/js/src/instrumentation/providers/flue-instrumentation.ts +++ b/js/src/instrumentation/providers/flue-instrumentation.ts @@ -1,11 +1,10 @@ import { toLoggedError } from "../core"; import { debugLogger } from "../../debug-logger"; import { - BRAINTRUST_CURRENT_SPAN_STORE, NOOP_SPAN, flush, _internalGetGlobalState, - startSpan as startBaseSpan, + _internalStartSpan as startBaseSpan, withCurrent, } from "../../logger"; import type { Span, StartSpanArgs } from "../../logger"; @@ -1308,9 +1307,7 @@ function runWithCurrentSpanStore( ): Promise { const state = _internalGetGlobalState(); const contextManager = state?.contextManager; - const currentSpanStore = contextManager - ? Reflect.get(contextManager, BRAINTRUST_CURRENT_SPAN_STORE) - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (contextManager && typeof currentSpanStore?.run === "function") { return currentSpanStore.run(contextManager.wrapSpanForStore(span), next); diff --git a/js/src/instrumentation/providers/genkit-instrumentation.ts b/js/src/instrumentation/providers/genkit-instrumentation.ts index e485295ea..16abe033c 100644 --- a/js/src/instrumentation/providers/genkit-instrumentation.ts +++ b/js/src/instrumentation/providers/genkit-instrumentation.ts @@ -8,7 +8,6 @@ import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { _internalGetGlobalState, - BRAINTRUST_CURRENT_SPAN_STORE, startSpan as startBaseSpan, } from "../../logger"; import type { CurrentSpanStore, Span } from "../../logger"; @@ -291,13 +290,7 @@ function bindActionCurrentSpanStoreToStart< ) => void; } & object) | undefined; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!startChannel?.bindStore || !currentSpanStore) { return; diff --git a/js/src/instrumentation/providers/github-copilot-instrumentation.ts b/js/src/instrumentation/providers/github-copilot-instrumentation.ts index b5f14976f..bf53ebd72 100644 --- a/js/src/instrumentation/providers/github-copilot-instrumentation.ts +++ b/js/src/instrumentation/providers/github-copilot-instrumentation.ts @@ -1,5 +1,5 @@ import type { IsoChannelHandlers } from "../../isomorph"; -import { startSpan as startBaseSpan } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/google-adk-instrumentation.test.ts b/js/src/instrumentation/providers/google-adk-instrumentation.test.ts index f4efcfc7d..12c8a9175 100644 --- a/js/src/instrumentation/providers/google-adk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-adk-instrumentation.test.ts @@ -1,10 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -const { - mockCurrentSpanStoreSymbol: MOCK_CURRENT_SPAN_STORE_SYMBOL, - mockInternalGetGlobalState, -} = vi.hoisted(() => ({ - mockCurrentSpanStoreSymbol: Symbol.for("braintrust.currentSpanStore"), +const { mockInternalGetGlobalState } = vi.hoisted(() => ({ mockInternalGetGlobalState: vi.fn(() => undefined), })); @@ -31,7 +27,6 @@ vi.mock("../../logger", () => ({ startSpan: (...args: any[]) => (mockStartSpan as any)(...args), _internalGetGlobalState: (...args: any[]) => (mockInternalGetGlobalState as any)(...args), - BRAINTRUST_CURRENT_SPAN_STORE: MOCK_CURRENT_SPAN_STORE_SYMBOL, withCurrent: (_span: any, callback: () => unknown) => callback(), Attachment: class MockAttachment { reference: any; @@ -183,7 +178,7 @@ describe("registerGoogleADKInstrumentation", () => { const wrapSpanForStore = vi.fn(() => "wrapped-runner-store"); mockInternalGetGlobalState.mockReturnValue({ contextManager: { - [MOCK_CURRENT_SPAN_STORE_SYMBOL]: currentSpanStore, + getCurrentSpanStore: () => currentSpanStore, wrapSpanForStore, }, } as any); @@ -492,7 +487,7 @@ describe("registerGoogleADKInstrumentation", () => { const wrapSpanForStore = vi.fn(() => "wrapped-agent-store"); mockInternalGetGlobalState.mockReturnValue({ contextManager: { - [MOCK_CURRENT_SPAN_STORE_SYMBOL]: currentSpanStore, + getCurrentSpanStore: () => currentSpanStore, wrapSpanForStore, }, } as any); diff --git a/js/src/instrumentation/providers/google-adk-instrumentation.ts b/js/src/instrumentation/providers/google-adk-instrumentation.ts index 994779305..de631de88 100644 --- a/js/src/instrumentation/providers/google-adk-instrumentation.ts +++ b/js/src/instrumentation/providers/google-adk-instrumentation.ts @@ -2,7 +2,6 @@ import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { - BRAINTRUST_CURRENT_SPAN_STORE, _internalGetGlobalState, startSpan as startBaseSpan, withCurrent, @@ -518,13 +517,7 @@ function bindCurrentSpanStoreToStart< ) => void; } & object) | undefined; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!startChannel?.bindStore || !currentSpanStore) { return; diff --git a/js/src/instrumentation/providers/google-genai-instrumentation.ts b/js/src/instrumentation/providers/google-genai-instrumentation.ts index 85e5ae049..bd3029147 100644 --- a/js/src/instrumentation/providers/google-genai-instrumentation.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.ts @@ -8,7 +8,6 @@ import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { _internalGetGlobalState, Attachment, - BRAINTRUST_CURRENT_SPAN_STORE, startSpan as startBaseSpan, type CurrentSpanStore, type Span, @@ -356,13 +355,7 @@ function bindCurrentSpanStoreToStart< ) => void; } & object) | undefined; - const currentSpanStore = contextManager - ? ( - contextManager as { - [BRAINTRUST_CURRENT_SPAN_STORE]?: CurrentSpanStore; - } - )[BRAINTRUST_CURRENT_SPAN_STORE] - : undefined; + const currentSpanStore = contextManager?.getCurrentSpanStore(); if (!startChannel?.bindStore || !currentSpanStore) { return; diff --git a/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts b/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts index f7250bb3b..c919b6d3b 100644 --- a/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts @@ -12,6 +12,7 @@ vi.mock("../../isomorph", () => ({ vi.mock("../../logger", () => ({ startSpan: (...args: unknown[]) => mockStartSpan(...args), + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), })); import iso from "../../isomorph"; diff --git a/js/src/instrumentation/providers/openai-codex-instrumentation.ts b/js/src/instrumentation/providers/openai-codex-instrumentation.ts index abf442120..eec4a36a2 100644 --- a/js/src/instrumentation/providers/openai-codex-instrumentation.ts +++ b/js/src/instrumentation/providers/openai-codex-instrumentation.ts @@ -2,8 +2,8 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; -import { startSpan as startBaseSpan } from "../../logger"; -import type { Span, StartSpanArgs } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; +import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -27,6 +27,8 @@ import type { OpenAICodexWebSearchItem, } from "../../vendor-sdk-types/openai-codex"; +type InternalStartSpanArgs = NonNullable[0]>; + type CodexRunState = { activeLlmSpan?: CodexLlmSpanState; activeItemSpans: Map; @@ -565,7 +567,7 @@ async function itemSpanArgs( item: OpenAICodexThreadItem, ): Promise< | { - start: StartSpanArgs; + start: InternalStartSpanArgs; end: Parameters[0]; } | undefined diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts index 7e7d406ed..48b38ea1f 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts @@ -27,6 +27,7 @@ vi.mock("../../isomorph", async (importOriginal) => { vi.mock("../../logger", () => ({ startSpan: (...args: unknown[]) => mockStartSpan(...args), + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), })); import { isAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts index 11829e004..22537472b 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts @@ -2,7 +2,7 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import iso, { type IsoAsyncLocalStorage } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; -import { startSpan as startBaseSpan } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/registry.ts b/js/src/instrumentation/registry.ts index 010aee634..60520f46c 100644 --- a/js/src/instrumentation/registry.ts +++ b/js/src/instrumentation/registry.ts @@ -135,7 +135,7 @@ export const registry = new InstrumentationRegistry(); * * @example * ```typescript - * import { configureInstrumentation } from 'braintrust'; + * import { configureInstrumentation } from 'braintrust/instrumentation'; * * // Disable OpenAI instrumentation * configureInstrumentation({ diff --git a/js/src/logger-misc.test.ts b/js/src/logger-misc.test.ts index bd7051544..730bacb68 100644 --- a/js/src/logger-misc.test.ts +++ b/js/src/logger-misc.test.ts @@ -10,7 +10,7 @@ import { BaseAttachment, Attachment, ExternalAttachment, - initExperiment, + init, initLogger, NOOP_SPAN, permalink, @@ -368,7 +368,7 @@ describe("span.link", () => { expect(state.appUrl).toBeDefined(); // Create a test experiment - const experiment = initExperiment("test-experiment"); + const experiment = init({ project: "test-experiment" }); // Get a span within the experiment context const span = experiment.startSpan({ @@ -417,7 +417,7 @@ describe("span.link", () => { test("span.link handles missing experiment id", async () => { await _exportsForTestingOnly.simulateLoginForTests(); - const experiment = initExperiment("test-experiment"); + const experiment = init({ project: "test-experiment" }); const span = experiment.startSpan({ name: "test-span" }); span.end(); // Force parentObjectId to be undefined diff --git a/js/src/logger.test.ts b/js/src/logger.test.ts index 1dffdbb49..6b67268fd 100644 --- a/js/src/logger.test.ts +++ b/js/src/logger.test.ts @@ -11,9 +11,11 @@ import { } from "vitest"; import { _exportsForTestingOnly, + _internalStartSpan, init, initDataset, initLogger, + extractTraceContextFromHeaders, Prompt, BraintrustState, loadPrompt, @@ -898,7 +900,8 @@ test("legacy initDataset applies bt eval internal BTQL runtime value", async () }, }); - const dataset = initDataset("test-project", { + const dataset = initDataset({ + project: "test-project", dataset: "test-dataset", state, }); @@ -2997,7 +3000,7 @@ describe("parent precedence", () => { test("withParent + wrapTraced: child spans attach to current span (not directly to withParent)", async () => { const logger = initLogger({ projectName: "test", projectId: "pid" }); const outer = logger.startSpan({ name: "outer" }); - const parentStr = await outer.export(); + const parentContext = extractTraceContextFromHeaders(outer.inject())!; outer.end(); const inner = wrapTraced( @@ -3007,7 +3010,7 @@ describe("parent precedence", () => { { name: "inner" }, ); - await withParent(parentStr, () => inner()); + await withParent(parentContext, () => inner()); await memory.flush(); const events = await memory.drain(); @@ -3051,7 +3054,7 @@ describe("parent precedence", () => { const inner = wrapTraced( async function inner() { - startSpan({ name: "forced", parent: parentStr }).end(); + _internalStartSpan({ name: "forced", parent: parentStr }).end(); }, { name: "inner" }, ); @@ -3083,7 +3086,7 @@ describe("parent precedence", () => { const child = secondaryLogger.startSpan({ name: "child", - parent: parentStr, + parent: parentStr as never, }); child.end(); @@ -3114,7 +3117,7 @@ describe("parent precedence", () => { const child = secondaryExperiment.startSpan({ name: "child", - parent: parentStr, + parent: parentStr as never, }); child.end(); diff --git a/js/src/logger.ts b/js/src/logger.ts index 793cb96fc..62000ba17 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -37,8 +37,7 @@ import { batchItems, constructJsonArray, DatasetRecord, - DEFAULT_IS_LEGACY_DATASET, - ensureDatasetRecord, + ensureNewDatasetRecord, ExperimentEvent, ExperimentLogFullArgs, ExperimentLogPartialArgs, @@ -313,17 +312,20 @@ export type StartSpanArgs = { spanAttributes?: Record; startTime?: number; /** - * The parent to start this span under. May be an exported span slug string - * (from `span.export()`) or an opaque W3C trace-context (from - * {@link extractTraceContextFromHeaders}). + * An opaque W3C trace context returned by + * {@link extractTraceContextFromHeaders}. */ - parent?: string | PropagationContext; + parent?: PropagationContext; event?: StartSpanEventArgs; propagatedEvent?: StartSpanEventArgs; spanId?: string; parentSpanIds?: ParentSpanIds | MultiParentSpanIds; }; +type InternalStartSpanArgs = Omit & { + parent?: string | PropagationContext; +}; + export type EndSpanArgs = { endTime?: number; }; @@ -431,8 +433,8 @@ export interface Span extends Exportable { * parameters of {@link Span.startSpan} for usage details. * * Callers should treat the return value as opaque. The serialization format - * may change from time to time. If parsing is needed, use - * `SpanComponentsV3.fromStr`. + * may change from time to time. For cross-service propagation, prefer + * {@link Span.inject} and {@link extractTraceContextFromHeaders}. * * @returns Serialized representation of this span's identifiers. */ @@ -489,11 +491,6 @@ export interface Span extends Exportable { */ flush(): Promise; - /** - * Alias for `end`. - */ - close(args?: EndSpanArgs): number; - /** * Set the span's name, type, or other attributes after it's created. */ @@ -518,12 +515,8 @@ export interface Span extends Exportable { kind: "span"; } -export const BRAINTRUST_CURRENT_SPAN_STORE = Symbol.for( - "braintrust.currentSpanStore", -); - /** - * The type of AsyncLocalStorage exposed via {@link BRAINTRUST_CURRENT_SPAN_STORE}. + * The AsyncLocalStorage contract used by context-manager integrations. * * The stored value is intentionally opaque (`unknown`) because the concrete type * depends on the active context manager: @@ -540,6 +533,14 @@ export abstract class ContextManager { abstract runInContext(span: Span, callback: () => R): R; abstract getCurrentSpan(): Span | undefined; + /** + * Return the store used by instrumentation hooks to propagate the current + * span, when the context manager supports direct store binding. + */ + getCurrentSpanStore(): CurrentSpanStore | undefined { + return undefined; + } + /** * Returns the value to store in the ALS bound to a global hook's start event. * In default mode this is the Span itself; in OTEL mode it is the OTEL Context @@ -553,12 +554,14 @@ export abstract class ContextManager { class BraintrustContextManager extends ContextManager { private _currentSpan: IsoAsyncLocalStorage; - [BRAINTRUST_CURRENT_SPAN_STORE]: IsoAsyncLocalStorage; constructor() { super(); this._currentSpan = iso.newAsyncLocalStorage(); - this[BRAINTRUST_CURRENT_SPAN_STORE] = this._currentSpan; + } + + getCurrentSpanStore(): CurrentSpanStore { + return this._currentSpan; } getParentSpanIds(): ContextParentSpanIds | undefined { @@ -582,11 +585,8 @@ class BraintrustContextManager extends ContextManager { } } -// make sure to update @braintrust/otel package declare global { var BRAINTRUST_CONTEXT_MANAGER: (new () => ContextManager) | undefined; - var BRAINTRUST_ID_GENERATOR: (new () => IDGenerator) | undefined; - var BRAINTRUST_SPAN_COMPONENT: SpanComponent | undefined; } type SpanComponent = typeof SpanComponentsV3 | typeof SpanComponentsV4; @@ -597,11 +597,7 @@ type SpanComponent = typeof SpanComponentsV3 | typeof SpanComponentsV4; // serialize as V4, legacy UUID IDs serialize as V3. These must move together -- // serializing hex IDs via V3 would lose the compact encoding and risk // corrupting hex values that happen to parse as UUIDs. An explicit -// `globalThis.BRAINTRUST_SPAN_COMPONENT` (e.g. from `@braintrust/otel`) wins. function getSpanComponentsClass(): SpanComponent { - if (globalThis.BRAINTRUST_SPAN_COMPONENT) { - return globalThis.BRAINTRUST_SPAN_COMPONENT; - } return resolveUseLegacyUuidIds() ? SpanComponentsV3 : SpanComponentsV4; } @@ -611,6 +607,20 @@ export function getContextManager(): ContextManager { : new BraintrustContextManager(); } +/** + * Configure the context manager used by Braintrust. + * + * This extension point is intended for runtime integrations such as + * `@braintrust/otel` and `@braintrust/browser`. Passing `undefined` restores + * Braintrust's default context manager. + */ +export function configureContextManager( + contextManager: (new () => ContextManager) | undefined, +): void { + globalThis.BRAINTRUST_CONTEXT_MANAGER = contextManager; + _globalState?.[RESET_CONTEXT_MANAGER_STATE](); +} + /** * A fake implementation of the Span API which does nothing. This can be used as the default span. */ @@ -674,10 +684,6 @@ export class NoopSpan implements Span { public async flush(): Promise {} - public close(args?: EndSpanArgs): number { - return this.end(args); - } - public setAttributes(_args: Omit) {} public startSpanWithParents( @@ -3851,38 +3857,7 @@ type InitializedExperiment = */ export function init( options: Readonly>, -): InitializedExperiment; - -/** - * Legacy form of `init` which accepts the project name as the first parameter, - * separately from the remaining options. See `init(options)` for full details. - */ -export function init( - project: string, - options?: Readonly>, -): InitializedExperiment; - -/** - * Combined overload implementation of `init`. Do not call this directly. - * Instead, call `init(options)` or `init(project, options)`. - */ -export function init( - projectOrOptions: string | Readonly>, - optionalOptions?: Readonly>, ): InitializedExperiment { - const options = ((): Readonly> => { - if (typeof projectOrOptions === "string") { - return { ...optionalOptions, project: projectOrOptions }; - } else { - if (optionalOptions !== undefined) { - throw new Error( - "Cannot specify options struct as both parameters. Must call either init(project, options) or init(options).", - ); - } - return projectOrOptions; - } - })(); - const { project, experiment, @@ -4111,66 +4086,6 @@ export function init( return ret as InitializedExperiment; } -/** - * Alias for init(options). - */ -export function initExperiment( - options: Readonly>, -): InitializedExperiment; - -/** - * Alias for init(project, options). - */ -export function initExperiment( - project: string, - options?: Readonly>, -): InitializedExperiment; - -/** - * Combined overload implementation of `initExperiment`, which is an alias for - * `init`. Do not call this directly. Instead, call `initExperiment(options)` or - * `initExperiment(project, options)`. - */ -export function initExperiment( - projectOrOptions: string | Readonly>, - optionalOptions?: Readonly>, -): InitializedExperiment { - const options = ((): Readonly> => { - if (typeof projectOrOptions === "string") { - return { ...optionalOptions, project: projectOrOptions }; - } else { - if (optionalOptions !== undefined) { - throw new Error( - "Cannot specify options struct as both parameters. Must call either init(project, options) or init(options).", - ); - } - return projectOrOptions; - } - })(); - return init(options); -} - -/** - * @deprecated Use {@link init} instead. - */ -export function withExperiment( - project: string, - callback: (experiment: Experiment) => R, - options: Readonly & SetCurrentArg> = {}, -): R { - debugLogger - .forState(options.state) - .warn( - "withExperiment is deprecated and will be removed in a future version of braintrust. Simply create the experiment with `init`.", - ); - const experiment = init(project, options); - return callback(experiment); -} - -type UseOutputOption = { - useOutput?: IsLegacyDataset; -}; - declare global { // Set by the bt eval runner when CLI-controlled BTQL should be pushed down // into dataset-backed evals. @@ -4178,22 +4093,21 @@ declare global { var __bt_eval_internal_btql: Record | undefined; } -export type InitDatasetOptions = - FullLoginOptions & { - dataset?: string; - description?: string; - version?: string; - environment?: string; - snapshotName?: string; - projectId?: string; - metadata?: Record; - state?: BraintrustState; - _internal_btql?: Record; - } & UseOutputOption; +export type InitDatasetOptions = FullLoginOptions & { + dataset?: string; + description?: string; + version?: string; + environment?: string; + snapshotName?: string; + projectId?: string; + metadata?: Record; + state?: BraintrustState; + _internal_btql?: Record; +}; -export type FullInitDatasetOptions = { +export type FullInitDatasetOptions = { project?: string; -} & InitDatasetOptions; +} & InitDatasetOptions; async function getDatasetSnapshots( params: @@ -4416,52 +4330,11 @@ async function serializeDatasetForExperiment({ * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. * @param options.projectId The id of the project to create the dataset in. This takes precedence over `project` if specified. * @param options.metadata A dictionary with additional data about the dataset. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. - * @param options.useOutput (Deprecated) If true, records will be fetched from this dataset in the legacy format, with the "expected" field renamed to "output". This option will be removed in a future version of Braintrust. * @returns The newly created Dataset. */ -export function initDataset< - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, ->( - options: Readonly>, -): Dataset; - -/** - * Legacy form of `initDataset` which accepts the project name as the first - * parameter, separately from the remaining options. - * - * See `initDataset(options)` for full details. - */ -export function initDataset< - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, ->( - project: string, - options?: Readonly>, -): Dataset; - -/** - * Combined overload implementation of `initDataset`. Do not call this - * directly. Instead, call `initDataset(options)` or `initDataset(project, - * options)`. - */ -export function initDataset< - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, ->( - projectOrOptions: string | Readonly>, - optionalOptions?: Readonly>, -): Dataset { - const options = ((): Readonly> => { - if (typeof projectOrOptions === "string") { - return { ...optionalOptions, project: projectOrOptions }; - } else { - if (optionalOptions !== undefined) { - throw new Error( - "Cannot specify options struct as both parameters. Must call either initDataset(project, options) or initDataset(options).", - ); - } - return projectOrOptions; - } - })(); - +export function initDataset( + options: Readonly, +): Dataset { const { project, dataset, @@ -4476,7 +4349,6 @@ export function initDataset< forceLogin, projectId, metadata, - useOutput: legacy, state: stateArg, _internal_btql, } = options; @@ -4563,7 +4435,6 @@ export function initDataset< stateArg ?? _globalState, lazyMetadata, typeof resolvedVersion === "string" ? resolvedVersion : undefined, - legacy, internalBtql, resolvedVersion instanceof LazyValue || normalizedEnvironment !== undefined || @@ -4591,26 +4462,6 @@ export function initDataset< return datasetObject; } -/** - * @deprecated Use {@link initDataset} instead. - */ -export function withDataset< - R, - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, ->( - project: string, - callback: (dataset: Dataset) => R, - options: Readonly> = {}, -): R { - debugLogger - .forState(options.state) - .warn( - "withDataset is deprecated and will be removed in a future version of braintrust. Simply create the dataset with `initDataset`.", - ); - const dataset = initDataset(project, options); - return callback(dataset); -} - // Note: the argument names *must* serialize the same way as the argument names // for the corresponding python function, because this function may be invoked // from arguments serialized elsewhere. @@ -6242,12 +6093,6 @@ export function wrapTraced any>( }, spanArgs)) as F; } -/** - * A synonym for `wrapTraced`. If you're porting from systems that use `traceable`, you can use this to - * make your codebase more consistent. - */ -export const traceable = wrapTraced; - /** * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", @@ -6259,19 +6104,25 @@ export function startSpan(args?: StartSpanArgs & OptionalStateArg): Span { return startSpanImpl(args); } +export function _internalStartSpan( + args?: InternalStartSpanArgs & OptionalStateArg, +): Span { + return startSpanImpl(args); +} + /** @internal Start a span whose initial row is merged with concurrent writes. */ export function _internalStartSpanWithInitialMerge( - args?: StartSpanArgs & OptionalStateArg, + args?: InternalStartSpanArgs & OptionalStateArg, ): Span { return startSpanImpl({ ...args, [INITIAL_SPAN_WRITE_AS_MERGE]: true, - } as StartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg); + } as InternalStartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg); } /** @internal Start a span with SDK-controlled context fields. */ export function _internalStartSpanWithContext( - args: StartSpanArgs & OptionalStateArg, + args: InternalStartSpanArgs & OptionalStateArg, context: Record, ): Span { return startSpanImpl({ @@ -6299,7 +6150,7 @@ export function setFetch(fetch: typeof globalThis.fetch): void { } function startSpanImpl( - args?: StartSpanArgs & OptionalStateArg & InternalSpanContextArg, + args?: InternalStartSpanArgs & OptionalStateArg & InternalSpanContextArg, ): Span { const state = args?.state ?? _globalState; @@ -6346,7 +6197,10 @@ function startSpanImpl( propagatedState, }); } else { - return parentObject.startSpan(args); + // The internal entrypoint still accepts exported Braintrust parent slugs. + // Span implementations support that runtime form even though it is no + // longer part of their public method signature. + return parentObject.startSpan(args as StartSpanArgs); } } @@ -6433,6 +6287,14 @@ async function* asyncGeneratorWithCurrent( } export function withParent( + parent: PropagationContext, + callback: () => R, + state: BraintrustState | undefined = undefined, +): R { + return (state ?? _globalState).currentParent.run(parent, () => callback()); +} + +export function _internalWithParent( parent: string | PropagationContext, callback: () => R, state: BraintrustState | undefined = undefined, @@ -6536,22 +6398,11 @@ function validateAndSanitizeExperimentLogPartialArgs( } } - if ("input" in event && event.input && "inputs" in event && event.inputs) { - throw new Error( - "Only one of input or inputs (deprecated) can be specified. Prefer input.", - ); - } - if ("tags" in event && event.tags) { validateTags(event.tags); } - if ("inputs" in event) { - const { inputs, ...rest } = event; - return { input: inputs, ...rest }; - } else { - return { ...event }; - } + return { ...event }; } /** @@ -6762,18 +6613,9 @@ async function resolveAttachmentsToBase64>( // handling special fields like 'id'). function validateAndSanitizeExperimentLogFullArgs( event: ExperimentLogFullArgs, - hasDataset: boolean, ): ExperimentLogFullArgs { - if ( - ("input" in event && - !isEmpty(event.input) && - "inputs" in event && - !isEmpty(event.inputs)) || - (!("input" in event) && !("inputs" in event)) - ) { - throw new Error( - "Exactly one of input or inputs (deprecated) must be specified. Prefer input.", - ); + if (!("input" in event)) { + throw new Error("input must be specified"); } if (isEmpty(event.output)) { @@ -6783,14 +6625,6 @@ function validateAndSanitizeExperimentLogFullArgs( throw new Error("scores must be specified"); } - if (hasDataset && event.datasetRecordId === undefined) { - throw new Error("datasetRecordId must be specified when using a dataset"); - } else if (!hasDataset && event.datasetRecordId !== undefined) { - throw new Error( - "datasetRecordId cannot be specified when not using a dataset", - ); - } - return event; } @@ -7122,7 +6956,7 @@ export class Experiment ); } - event = validateAndSanitizeExperimentLogFullArgs(event, !!this.dataset); + event = validateAndSanitizeExperimentLogFullArgs(event); const span = this.startSpanImpl({ startTime: this.lastStartTime, event }); this.lastStartTime = span.end(); return span.id; @@ -7367,18 +7201,6 @@ export class Experiment async flush(): Promise { return await this.state.bgLogger().flush(); } - - /** - * @deprecated This function is deprecated. You can simply remove it from your code. - */ - public async close(): Promise { - debugLogger - .forState(this.state) - .warn( - "close is deprecated and will be removed in a future version of braintrust. It is now a no-op and can be removed", - ); - return this.id; - } } /** @@ -8002,10 +7824,6 @@ export class SpanImpl implements Span { return await this._state.bgLogger().flush(); } - public close(args?: EndSpanArgs): number { - return this.end(args); - } - public state(): BraintrustState { return this._state; } @@ -8086,9 +7904,7 @@ function splitLoggingData({ * * You should not create `Dataset` objects directly. Instead, use the `braintrust.initDataset()` method. */ -export class Dataset< - IsLegacyDataset extends boolean = typeof DEFAULT_IS_LEGACY_DATASET, -> extends ObjectFetcher> { +export class Dataset extends ObjectFetcher { private readonly lazyMetadata: LazyValue; private readonly __braintrust_dataset_marker = true; private newRecords = 0; @@ -8100,29 +7916,16 @@ export class Dataset< private state: BraintrustState, lazyMetadata: LazyValue, pinnedVersion?: string, - legacy?: IsLegacyDataset, _internal_btql?: Record, pinState?: DatasetPinState, ) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const isLegacyDataset = (legacy ?? - DEFAULT_IS_LEGACY_DATASET) as IsLegacyDataset; - if (isLegacyDataset) { - debugLogger - .forState(state) - .warn( - `Records will be fetched from this dataset in the legacy format, with the "expected" field renamed to "output". Please update your code to use "expected", and use \`braintrust.initDataset()\` with \`{ useOutput: false }\`, which will become the default in a future version of Braintrust.`, - ); - } super( "dataset", pinnedVersion, (r: AnyDatasetRecord) => - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ensureDatasetRecord( + ensureNewDatasetRecord( enrichAttachments(r, this.state), - isLegacyDataset, - ) as WithTransactionId>, + ) as WithTransactionId, _internal_btql, ); void this.__braintrust_dataset_marker; @@ -8213,13 +8016,9 @@ export class Dataset< private validateEvent({ metadata, - expected, - output, tags, }: { metadata?: Record; - expected?: unknown; - output?: unknown; tags?: string[]; }) { if (metadata !== undefined) { @@ -8230,12 +8029,6 @@ export class Dataset< } } - if (expected !== undefined && output !== undefined) { - throw new Error( - "Only one of expected or output (deprecated) can be specified. Prefer expected.", - ); - } - if (tags) { validateTags(tags); } @@ -8247,7 +8040,6 @@ export class Dataset< expected, metadata, tags, - output, origin, isMerge, }: { @@ -8256,18 +8048,15 @@ export class Dataset< expected?: unknown; metadata?: Record; tags?: string[]; - output?: unknown; origin?: ObjectReference; isMerge?: boolean; }): LazyValue { return new LazyValue(async () => { const dataset_id = await this.id; - const expectedValue = expected === undefined ? output : expected; - const args: BackgroundLogEvent = { id, input, - expected: expectedValue, + expected, tags, dataset_id, created: !isMerge ? new Date().toISOString() : undefined, //if we're merging/updating an event we will not add this ts @@ -8298,7 +8087,6 @@ export class Dataset< * JSON-serializable type, but its keys must be strings. * @param event.origin (Optional) a reference to the source object this dataset record was derived from. * @param event.id (Optional) a unique identifier for the event. If you don't provide one, Braintrust will generate one for you. - * @param event.output: (Deprecated) The output of your application. Use `expected` instead. * @returns The `id` of the logged record. */ public insert({ @@ -8307,7 +8095,6 @@ export class Dataset< metadata, tags, id, - output, origin, }: { readonly input?: unknown; @@ -8315,10 +8102,9 @@ export class Dataset< readonly tags?: string[]; readonly metadata?: Record; readonly id?: string; - readonly output?: unknown; readonly origin?: ObjectReference; }): string { - this.validateEvent({ metadata, expected, output, tags }); + this.validateEvent({ metadata, tags }); const rowId = id || uuidv4(); const args = this.createArgs( @@ -8328,7 +8114,6 @@ export class Dataset< expected, metadata, tags, - output, origin, isMerge: false, }), @@ -8365,7 +8150,7 @@ export class Dataset< readonly tags?: string[]; readonly metadata?: Record; }): string { - this.validateEvent({ metadata, expected, tags }); + this.validateEvent({ metadata, tags }); const args = this.createArgs( deepCopyEvent({ @@ -8561,18 +8346,6 @@ export class Dataset< return await this.state.bgLogger().flush(); } - /** - * @deprecated This function is deprecated. You can simply remove it from your code. - */ - public async close(): Promise { - debugLogger - .forState(this.state) - .warn( - "close is deprecated and will be removed in a future version of braintrust. It is now a no-op and can be removed", - ); - return this.id; - } - public static isDataset(data: unknown): data is Dataset { return ( typeof data === "object" && @@ -9337,7 +9110,7 @@ export class RemoteEvalParameters< } } -export type AnyDataset = Dataset; +export type AnyDataset = Dataset; /** * Summary of a score's performance. diff --git a/js/src/node/index.ts b/js/src/node/index.ts index 60e1efe09..e4a44adfe 100644 --- a/js/src/node/index.ts +++ b/js/src/node/index.ts @@ -56,4 +56,3 @@ import { configureNode } from "./config"; configureNode(); export * from "../exports"; -export * as default from "../exports"; diff --git a/js/src/openai-utils.ts b/js/src/openai-utils.ts index 5fa850915..9bc0cfa9c 100644 --- a/js/src/openai-utils.ts +++ b/js/src/openai-utils.ts @@ -5,8 +5,8 @@ import { isObject } from "../util/index"; // when OpenAI usage data does not include it. export const BRAINTRUST_CACHED_STREAM_METRIC = "__braintrust_cached_metric"; -export const LEGACY_CACHED_HEADER = "x-cached"; -export const X_CACHED_HEADER = "x-bt-cached"; +const LEGACY_CACHED_HEADER = "x-cached"; +const X_CACHED_HEADER = "x-bt-cached"; /** * Token name mappings for OpenAI metrics. @@ -63,7 +63,7 @@ export function parseMetricsFromUsage(usage: unknown): Record { return metrics; } -export function parseCachedHeader( +function parseCachedHeader( value: string | null | undefined, ): number | undefined { if (!value) { diff --git a/js/src/package-exports.test.ts b/js/src/package-exports.test.ts new file mode 100644 index 000000000..217d05ad7 --- /dev/null +++ b/js/src/package-exports.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, test } from "vitest"; + +const packageJson = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), +) as { exports: Record }; + +describe("package exports", () => { + test("publishes the curated entrypoints", () => { + expect(Object.keys(packageJson.exports).sort()).toEqual( + [ + ".", + "./apply-auto-instrumentation", + "./edge-light", + "./esbuild", + "./hook.mjs", + "./instrumentation", + "./next", + "./node", + "./package.json", + "./rollup", + "./vite", + "./vitest-evals-reporter", + "./webpack", + "./workerd", + ].sort(), + ); + }); + + test("does not publish legacy or implementation entrypoints", () => { + expect(packageJson.exports).not.toHaveProperty("./browser"); + expect(packageJson.exports).not.toHaveProperty("./util"); + expect(packageJson.exports).not.toHaveProperty("./webpack-loader"); + expect(packageJson.exports).not.toHaveProperty("./internal"); + expect(packageJson.exports).not.toHaveProperty("./internal/webpack-loader"); + }); +}); diff --git a/js/src/prompt-schemas.ts b/js/src/prompt-schemas.ts index 46ad5f307..c83c420f0 100644 --- a/js/src/prompt-schemas.ts +++ b/js/src/prompt-schemas.ts @@ -1,9 +1,3 @@ -import { z } from "zod/v3"; -import { - ToolFunctionDefinition as toolFunctionDefinitionSchema, - ChatCompletionMessageParam as chatCompletionMessageParamSchema, - ModelParams as modelParamsSchema, -} from "./generated_types"; import type { ToolFunctionDefinitionType as ToolFunctionDefinition, ChatCompletionMessageParamType, @@ -17,20 +11,6 @@ export type PromptContents = | { prompt: string } | { messages: ChatCompletionMessageParamType[] }; -const internalPromptContentsSchema = z.union([ - z.object({ - prompt: z.string(), - }), - z.object({ - messages: z.array(chatCompletionMessageParamSchema), - }), -]); -export const promptContentsSchema: z.ZodType< - PromptContents, - z.ZodTypeDef, - unknown -> = internalPromptContentsSchema; - export type PromptDefinition = PromptContents & { model: string; params?: ModelParamsType; @@ -38,36 +18,10 @@ export type PromptDefinition = PromptContents & { environments?: string[]; }; -const internalPromptDefinitionSchema = internalPromptContentsSchema.and( - z.object({ - model: z.string(), - params: modelParamsSchema.optional(), - templateFormat: z.enum(["mustache", "nunjucks", "none"]).optional(), - environments: z.array(z.string()).optional(), - }), -); -export const promptDefinitionSchema: z.ZodType< - PromptDefinition, - z.ZodTypeDef, - unknown -> = internalPromptDefinitionSchema; - export type PromptDefinitionWithTools = PromptDefinition & { tools?: ToolFunctionDefinition[]; }; -const internalPromptDefinitionWithToolsSchema = - internalPromptDefinitionSchema.and( - z.object({ - tools: z.array(toolFunctionDefinitionSchema).optional(), - }), - ); -export const promptDefinitionWithToolsSchema: z.ZodType< - PromptDefinitionWithTools, - z.ZodTypeDef, - unknown -> = internalPromptDefinitionWithToolsSchema; - export function promptDefinitionToPromptData( promptDefinition: PromptDefinition, rawTools?: ToolFunctionDefinition[], diff --git a/js/src/propagation.test.ts b/js/src/propagation.test.ts index 17b07dbac..936dfbd84 100644 --- a/js/src/propagation.test.ts +++ b/js/src/propagation.test.ts @@ -10,6 +10,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { _exportsForTestingOnly, + _internalStartSpan, _injectIntoCarrier, extractTraceContextFromHeaders, initLogger, @@ -1026,14 +1027,14 @@ describe("inject / extract / round-trip", () => { root_span_id: pRoot, }).toStr(); - const child = logger.startSpan({ name: "child", parent: slug }); + const child = _internalStartSpan({ name: "child", parent: slug }); expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); child.end(); }); test("legacy parent slug (UUID) linked in hex mode", () => { - const logger = makeLogger(); + makeLogger(); const pSpan = uuidv4(); const pRoot = uuidv4(); const legacySlug = new SpanComponentsV3({ @@ -1044,7 +1045,7 @@ describe("inject / extract / round-trip", () => { root_span_id: pRoot, }).toStr(); - const child = logger.startSpan({ name: "child", parent: legacySlug }); + const child = _internalStartSpan({ name: "child", parent: legacySlug }); // Links to the slug's UUID ids; the child's own span id stays hex. expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); @@ -1064,7 +1065,7 @@ describe("inject / extract / round-trip", () => { root_span_id: pRoot, }).toStr(); - const child = startSpan({ name: "child", parent: legacySlug }); + const child = _internalStartSpan({ name: "child", parent: legacySlug }); expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); expect(child.spanId.length).toBe(16); @@ -1221,7 +1222,7 @@ describe("legacy UUID mode", () => { }); test("legacy parent slug (UUID) linked in legacy mode", () => { - const logger = initLogger({ projectName: "legacy-proj" }); + initLogger({ projectName: "legacy-proj" }); const pSpan = uuidv4(); const pRoot = uuidv4(); const legacySlug = new SpanComponentsV3({ @@ -1232,14 +1233,14 @@ describe("legacy UUID mode", () => { root_span_id: pRoot, }).toStr(); - const child = logger.startSpan({ name: "child", parent: legacySlug }); + const child = _internalStartSpan({ name: "child", parent: legacySlug }); expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); child.end(); }); test("hex parent slug linked in legacy mode", () => { - const logger = initLogger({ projectName: "legacy-proj" }); + initLogger({ projectName: "legacy-proj" }); const pSpan = "00f067aa0ba902b7"; // 8-byte hex const pRoot = "4bf92f3577b34da6a3ce929d0e0e4736"; // 16-byte hex const hexSlug = new SpanComponentsV4({ @@ -1250,7 +1251,7 @@ describe("legacy UUID mode", () => { root_span_id: pRoot, }).toStr(); - const child = logger.startSpan({ name: "child", parent: hexSlug }); + const child = _internalStartSpan({ name: "child", parent: hexSlug }); // Links to the slug's hex ids; the child's own span id stays UUID. expect(child.rootSpanId).toBe(pRoot); expect(child.spanParents).toEqual([pSpan]); diff --git a/js/src/propagation.ts b/js/src/propagation.ts index a8246cce1..1733547aa 100644 --- a/js/src/propagation.ts +++ b/js/src/propagation.ts @@ -26,7 +26,7 @@ const DEFAULT_TRACE_FLAGS = "01"; * not sampled), kept raw so any future flag bits survive a parse -> format * round trip without per-bit handling. */ -export interface ParsedTraceparent { +interface ParsedTraceparent { traceId: string; spanId: string; traceFlags: string; diff --git a/js/src/public-types.test.ts b/js/src/public-types.test.ts index 0583994df..3a37a9cf7 100644 --- a/js/src/public-types.test.ts +++ b/js/src/public-types.test.ts @@ -1,69 +1,84 @@ -import { expectTypeOf, test } from "vitest"; -import { z } from "zod/v3"; +import { expect, expectTypeOf, test } from "vitest"; -import { - AttachmentReference, - braintrustStreamChunkSchema, - logs3OverflowUploadSchema, - promptContentsSchema, - promptDefinitionSchema, - promptDefinitionWithToolsSchema, - type BraintrustStreamChunk, - type EvalParameters, - type Logs3OverflowUpload, - type PromptContents, - type PromptDefinition, - type PromptDefinitionWithTools, +import * as publicExports from "./exports"; +import type { + ContextManager, + ContextParentSpanIds, + CurrentSpanStore, + Dataset, + DurableEvalStore, + EvalScorer, + EvalTask, + LangChainCallbackHandlerOptions, + PropagationContext, + Span, + StartSpanArgs, + TemplateRendererPlugin, + Trace, } from "./exports"; -import type { InferParameters } from "./eval-parameters"; -import type { AttachmentReferenceType } from "./generated_plain_types"; -import type { Prompt } from "./logger"; -import { - spanComponentsV3Schema, - type SpanComponentsV3Data, -} from "../util/span_identifier_v3"; import { - spanComponentsV4Schema, - type SpanComponentsV4Data, -} from "../util/span_identifier_v4"; + BraintrustLangChainCallbackHandler, + init, + initDataset, + withParent, +} from "./exports"; -test("exported validators preserve their public output types", () => { - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); +test("exports only intentional extension types", () => { + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf(publicExports.configureContextManager).toBeFunction(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf().toBeObject(); + expectTypeOf(BraintrustLangChainCallbackHandler).toBeConstructibleWith(); expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); - expectTypeOf< - z.infer - >().toEqualTypeOf(); + EvalTask + >().toBeFunction(); + expectTypeOf>().toBeFunction(); +}); + +test("does not expose runtime schemas or implementation helpers", () => { + for (const name of [ + "AttachmentReference", + "braintrustStreamChunkSchema", + "logs3OverflowUploadSchema", + "promptContentsSchema", + "promptDefinitionSchema", + "promptDefinitionWithToolsSchema", + "SpanImpl", + "IDGenerator", + "_exportsForTestingOnly", + "default", + ]) { + expect(publicExports).not.toHaveProperty(name); + } }); -test("evaluation parameters retain custom schema inference", () => { - const parameters = { - subject: z.string(), - model: { type: "model" as const }, - prompt: { type: "prompt" as const }, - } satisfies EvalParameters; +test("accepts only canonical v4 call shapes", () => { + if (false) { + init({ project: "project" }); + initDataset({ project: "project", dataset: "dataset" }); + + // @ts-expect-error The string-first overload was removed in v4. + init("project"); + // @ts-expect-error The string-first overload was removed in v4. + initDataset("project", { dataset: "dataset" }); + // @ts-expect-error Exported span slugs are internal integration plumbing. + withParent("exported-span", () => undefined); + + const dataset = null as unknown as Dataset; + // @ts-expect-error Dataset records use `expected`, not the legacy `output` alias. + dataset.insert({ input: "input", output: "expected" }); + + const span = null as unknown as Span; + // @ts-expect-error Spans end with `end()`; the `close()` alias was removed. + span.close(); + } - expectTypeOf>().toEqualTypeOf<{ - subject: string; - model: string; - prompt: Prompt; - }>(); + expect(true).toBe(true); }); diff --git a/js/src/sandbox.ts b/js/src/sandbox.ts index cfafa8c98..58c9f45be 100644 --- a/js/src/sandbox.ts +++ b/js/src/sandbox.ts @@ -7,7 +7,7 @@ import { type BraintrustState, _internalGetGlobalState } from "./logger"; * Configuration for a sandbox runtime. * @internal */ -export interface SandboxConfig { +interface SandboxConfig { /** The sandbox provider. Currently only "modal" is supported. */ provider: "modal"; /** Reference to the sandbox snapshot. */ @@ -18,7 +18,7 @@ export interface SandboxConfig { * Options for registering a sandbox function. * @internal */ -export interface RegisterSandboxOptions { +interface RegisterSandboxOptions { /** Group name for the sandbox functions. */ name: string; /** Name of the project to register the sandbox in. */ @@ -47,7 +47,7 @@ export interface RegisterSandboxOptions { * Result of registering a sandbox. * @internal */ -export interface RegisterSandboxResult { +interface RegisterSandboxResult { /** Project ID the sandbox is registered in. */ projectId: string; /** Registered eval functions discovered from this sandbox. */ diff --git a/js/src/template/registry.ts b/js/src/template/registry.ts index 6ebcace61..8c6faa704 100644 --- a/js/src/template/registry.ts +++ b/js/src/template/registry.ts @@ -97,7 +97,7 @@ class TemplatePluginRegistry { } } -export const templateRegistry = new TemplatePluginRegistry(); +const templateRegistry = new TemplatePluginRegistry(); /** * Register a template plugin and optionally activate it diff --git a/js/src/trace.ts b/js/src/trace.ts index d3e6b7610..fd40b6378 100644 --- a/js/src/trace.ts +++ b/js/src/trace.ts @@ -16,7 +16,7 @@ type SpanRecord = any; * Fetcher for spans by root_span_id, using the ObjectFetcher pattern. * Handles pagination automatically via cursor-based iteration. */ -export class SpanFetcher extends ObjectFetcher { +class SpanFetcher extends ObjectFetcher { constructor( objectType: "experiment" | "project_logs" | "playground_logs", private readonly _objectId: string, diff --git a/js/src/workerd/index.ts b/js/src/workerd/index.ts index 7f1f92300..3fa5cdb19 100644 --- a/js/src/workerd/index.ts +++ b/js/src/workerd/index.ts @@ -10,4 +10,3 @@ import { configureWorkerd } from "./config"; configureWorkerd(); export * from "../exports"; -export * as default from "../exports"; diff --git a/js/src/wrappers/ai-sdk/deprecated/BraintrustMiddleware.ts b/js/src/wrappers/ai-sdk/deprecated/BraintrustMiddleware.ts deleted file mode 100644 index d483ed02b..000000000 --- a/js/src/wrappers/ai-sdk/deprecated/BraintrustMiddleware.ts +++ /dev/null @@ -1,493 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/consistent-type-assertions */ -import { SpanTypeAttribute } from "../../../../util/index"; -import { startSpan, type CompiledPrompt } from "../../../logger"; -import { - INSTRUMENTATION_NAMES, - withSpanInstrumentationName, -} from "../../../span-origin"; -import { - extractAnthropicCacheTokens, - finalizeAnthropicTokens, - toNumericMetrics, -} from "../../anthropic-tokens-util"; -import { processInputAttachments } from "../../attachment-utils"; - -function detectProviderFromResult(result: { - providerMetadata?: Record; -}): string | undefined { - if (!result?.providerMetadata) { - return undefined; - } - - const keys = Object.keys(result.providerMetadata); - return keys?.at(0); -} - -function extractModelFromResult(result: { - response?: { modelId?: string }; - request?: { body?: { model?: string } }; -}): string | undefined { - if (result?.response?.modelId) { - return result.response.modelId; - } - - if (result?.request?.body?.model) { - return result.request.body.model; - } - - return undefined; -} - -function extractModelFromWrapGenerateCallback(model: { - modelId?: string; - config?: Record; - specificationVersion?: string; - provider?: string; - supportedUrls?: Record; -}): string | undefined { - return model?.modelId; -} - -function camelToSnake(str: string): string { - return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); -} - -function extractModelParameters( - params: Record, - excludeKeys: Set, -): Record { - const modelParams: Record = {}; - - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && !excludeKeys.has(key)) { - const snakeKey = camelToSnake(key); - modelParams[snakeKey] = value; - } - } - - return modelParams; -} - -function getNumberProperty(obj: unknown, key: string): number | undefined { - if (!obj || typeof obj !== "object" || !(key in obj)) { - return undefined; - } - const value = Reflect.get(obj, key); - return typeof value === "number" ? value : undefined; -} - -function normalizeUsageMetrics( - usage: unknown, - provider?: string, - providerMetadata?: Record, -): Record { - const metrics: Record = {}; - - // Standard AI SDK usage fields - const inputTokens = getNumberProperty(usage, "inputTokens"); - if (inputTokens !== undefined) { - metrics.prompt_tokens = inputTokens; - } - - const outputTokens = getNumberProperty(usage, "outputTokens"); - if (outputTokens !== undefined) { - metrics.completion_tokens = outputTokens; - } - - const totalTokens = getNumberProperty(usage, "totalTokens"); - if (totalTokens !== undefined) { - metrics.tokens = totalTokens; - } - - const reasoningTokens = getNumberProperty(usage, "reasoningTokens"); - if (reasoningTokens !== undefined) { - metrics.completion_reasoning_tokens = reasoningTokens; - } - - const cachedInputTokens = getNumberProperty(usage, "cachedInputTokens"); - if (cachedInputTokens !== undefined) { - metrics.prompt_cached_tokens = cachedInputTokens; - } - - // Anthropic-specific cache token handling - if (provider === "anthropic") { - const anthropicMetadata = providerMetadata?.anthropic as any; - - if (anthropicMetadata) { - const cacheReadTokens = - getNumberProperty(anthropicMetadata.usage, "cache_read_input_tokens") || - 0; - const cacheCreationTokens = - getNumberProperty( - anthropicMetadata.usage, - "cache_creation_input_tokens", - ) || 0; - - const cacheTokens = extractAnthropicCacheTokens( - cacheReadTokens, - cacheCreationTokens, - ); - Object.assign(metrics, cacheTokens); - - // Use the returned object: finalization can drop cache-creation metrics, - // and merging it back over `metrics` would keep them. - return toNumericMetrics(finalizeAnthropicTokens(metrics)); - } - } - - return metrics; -} - -function normalizeFinishReason(reason: any): string | undefined { - if (typeof reason !== "string") return undefined; - return reason.replace(/-/g, "_"); -} - -function buildAssistantOutputWithToolCalls(result: any, toolCalls: any[]) { - return [ - { - index: 0, - logprobs: null, - finish_reason: - normalizeFinishReason(result?.finishReason) ?? - (toolCalls.length ? "tool_calls" : undefined), - message: { - role: "assistant", - tool_calls: toolCalls.length > 0 ? toolCalls : undefined, - }, - }, - ]; -} - -function extractToolCallsFromSteps(steps: any[] | undefined) { - const toolCalls: any[] = []; - if (!Array.isArray(steps)) return toolCalls; - let idx = 0; - for (const step of steps) { - const blocks: any[] | undefined = (step as any)?.content; - if (!Array.isArray(blocks)) continue; - for (const block of blocks) { - if ( - block && - typeof block === "object" && - (block as any).type === "tool-call" - ) { - toolCalls.push({ - id: (block as any).toolCallId, - type: "function", - index: idx++, - function: { - name: (block as any).toolName, - arguments: - typeof (block as any).input === "string" - ? (block as any).input - : JSON.stringify((block as any).input ?? {}), - }, - }); - } - } - } - return toolCalls; -} - -function extractToolCallsFromBlocks(blocks: any[] | undefined) { - if (!Array.isArray(blocks)) return []; - return extractToolCallsFromSteps([{ content: blocks }] as any); -} - -function extractInput(params: any) { - return params?.prompt ?? params?.messages ?? params?.system; -} - -// Generic middleware interface that works with any AI SDK types -interface LanguageModelV2Middleware { - wrapGenerate?: (options: { - doGenerate: () => any; - doStream: () => any; - params: TCallOptions; - model: TModel; - }) => Promise; - wrapStream?: (options: { - doGenerate: () => any; - doStream: () => any; - params: TCallOptions; - model: TModel; - }) => Promise; -} - -/** - * Configuration options for the AI SDK middleware - */ -interface MiddlewareConfig { - /** Enable debug logging */ - debug?: boolean; - /** Name identifier for the middleware instance */ - name?: string; - /** Span info from loadPrompt for prompt version tracking */ - spanInfo?: CompiledPrompt<"chat">["span_info"]; -} - -// V2-specific exclude keys for extractModelParameters -const V2_EXCLUDE_KEYS = new Set([ - "prompt", // Already captured as input - "system", // Already captured as input - "messages", // Already captured as input - "model", // Already captured in metadata.model - "providerOptions", // Internal AI SDK configuration -]); - -/** - * Creates a Braintrust middleware for AI SDK v2 that automatically traces - * generateText and streamText calls with comprehensive metadata and metrics. - * - * @param config - Configuration options for the middleware - * @returns A middleware object compatible with AI SDK v2's wrapLanguageModel - * - * @deprecated The new `wrapAISDK` replaces this middleware. - * - * @example - * ```typescript - * import { wrapLanguageModel } from "ai"; - * import { openai } from "@ai-sdk/openai"; - * import { BraintrustMiddleware } from "braintrust"; - * - * const model = wrapLanguageModel({ - * model: openai("gpt-4"), - * middleware: BraintrustMiddleware({ debug: true, name: "MyMiddleware" }) - * }); - * ``` - * - */ -export function BraintrustMiddleware( - config: MiddlewareConfig = {}, -): LanguageModelV2Middleware { - return { - wrapGenerate: async ({ - doGenerate, - params, - model: modelFromWrapGenerate, - }) => { - // Extract and process input attachments - const rawInput = extractInput(params); - const processedInput = processInputAttachments(rawInput); - - const spanArgs = { - name: config.spanInfo?.name || "ai-sdk.doGenerate", - spanAttributes: { - type: SpanTypeAttribute.LLM, - ...(config.spanInfo?.spanAttributes || {}), - }, - event: { - input: processedInput, - metadata: { - ...extractModelParameters(params, V2_EXCLUDE_KEYS), - ...(config.spanInfo?.metadata || {}), - }, - }, - }; - - const span = startSpan( - withSpanInstrumentationName(spanArgs, INSTRUMENTATION_NAMES.AI_SDK), - ); - - try { - const result = await doGenerate(); - - const metadata: Record = {}; - - const provider = detectProviderFromResult(result); - if (provider !== undefined) { - metadata.provider = provider; - } - - if (result.finishReason !== undefined) { - metadata.finish_reason = result.finishReason; - } - - const model = extractModelFromResult(result); - if (model !== undefined) { - metadata.model = model; - } else if (modelFromWrapGenerate) { - // Use the model from the wrapGenerate call if it's not in the result - const modelId = extractModelFromWrapGenerateCallback( - modelFromWrapGenerate, - ); - if (modelId) { - metadata.model = modelId; - } - } - - let toolCalls = extractToolCallsFromSteps((result as any)?.steps); - if (!toolCalls || toolCalls.length === 0) { - toolCalls = extractToolCallsFromBlocks((result as any)?.content); - } - - span.log({ - output: - toolCalls.length > 0 - ? buildAssistantOutputWithToolCalls(result, toolCalls) - : (result as any)?.content, - metadata, - metrics: normalizeUsageMetrics( - result.usage, - provider, - result.providerMetadata, - ), - }); - - return result; - } catch (error) { - span.log({ - error: error instanceof Error ? error.message : String(error), - }); - throw error; - } finally { - span.end(); - } - }, - wrapStream: async ({ doStream, params }) => { - // Extract and process input attachments - const rawInput = extractInput(params); - const processedInput = processInputAttachments(rawInput); - - const spanArgs = { - name: config.spanInfo?.name || "ai-sdk.doStream", - spanAttributes: { - type: SpanTypeAttribute.LLM, - ...(config.spanInfo?.spanAttributes || {}), - }, - event: { - input: processedInput, - metadata: { - ...extractModelParameters(params, V2_EXCLUDE_KEYS), - ...(config.spanInfo?.metadata || {}), - }, - }, - }; - - const span = startSpan( - withSpanInstrumentationName(spanArgs, INSTRUMENTATION_NAMES.AI_SDK), - ); - - try { - const { stream, ...rest } = await doStream(); - - const textChunks: string[] = []; - const toolBlocks: any[] = []; - let finalUsage: unknown = {}; - let finalFinishReason: unknown = undefined; - let providerMetadata: Record = {}; - - const transformStream = new TransformStream({ - transform(chunk: any, controller: any) { - try { - // Collect text deltas - if (chunk.type === "text-delta" && chunk.delta) { - textChunks.push(chunk.delta); - } - - // Collect tool call/result blocks for formatting later - if (chunk.type === "tool-call" || chunk.type === "tool-result") { - toolBlocks.push(chunk); - } - - // Capture final metadata - if (chunk.type === "finish") { - finalFinishReason = chunk.finishReason; - finalUsage = chunk.usage || {}; - providerMetadata = chunk.providerMetadata || {}; - } - - controller.enqueue(chunk); - } catch (error) { - // Log stream processing error - span.log({ - error: error instanceof Error ? error.message : String(error), - }); - span.end(); - controller.error(error); - } - }, - - flush() { - try { - // Log the final aggregated result when stream completes - const generatedText = textChunks.join(""); - let output: unknown = generatedText - ? [{ type: "text", text: generatedText }] - : []; - - // Create a result object for provider detection - const resultForDetection = { - providerMetadata, - response: rest.response, - ...rest, - finishReason: finalFinishReason, - }; - - const metadata: Record = {}; - - const provider = detectProviderFromResult(resultForDetection); - if (provider !== undefined) { - metadata.provider = provider; - } - - if (finalFinishReason !== undefined) { - metadata.finish_reason = finalFinishReason; - } - - const model = extractModelFromResult(resultForDetection); - if (model !== undefined) { - metadata.model = model; - } - - // If tool calls streamed, prefer assistant tool_calls output - if (toolBlocks.length > 0) { - const toolCalls = extractToolCallsFromSteps([ - { content: toolBlocks }, - ] as any); - if (toolCalls.length > 0) { - output = buildAssistantOutputWithToolCalls( - resultForDetection, - toolCalls, - ); - } - } - - span.log({ - output, - metadata, - metrics: normalizeUsageMetrics( - finalUsage, - provider, - providerMetadata, - ), - }); - - span.end(); - } catch (error) { - // Log flush error - span.log({ - error: error instanceof Error ? error.message : String(error), - }); - span.end(); - throw error; - } - }, - }); - - return { - stream: stream.pipeThrough(transformStream), - ...rest, - }; - } catch (error) { - span.log({ - error: error instanceof Error ? error.message : String(error), - }); - span.end(); - throw error; - } - }, - }; -} diff --git a/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts b/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts deleted file mode 100644 index 705048346..000000000 --- a/js/src/wrappers/ai-sdk/deprecated/wrapAISDKModel.ts +++ /dev/null @@ -1,362 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-assertions */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { startSpan } from "../../../logger"; -import { - INSTRUMENTATION_NAMES, - withSpanInstrumentationName, -} from "../../../span-origin"; -import { getCurrentUnixTimestamp, isEmpty } from "../../../util"; -import { - LEGACY_CACHED_HEADER, - parseCachedHeader, - X_CACHED_HEADER, -} from "../../oai"; - -/** - * Wrap an ai-sdk model (created with `.chat()`, `.completion()`, etc.) to add tracing. If Braintrust is - * not configured, this is a no-op - * - * @param model - * @deprecated You should use the new `wrapAISDK`. - * @returns The wrapped object. - */ -export function wrapAISDKModel(model: T): T { - const m = model as any; - if ( - m?.specificationVersion === "v1" && - typeof m?.provider === "string" && - typeof m?.modelId === "string" - ) { - return new BraintrustLanguageModelWrapper(m) as any as T; - } else { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.warn("Unsupported AI SDK model. Not wrapping."); - return model; - } -} - -class BraintrustLanguageModelWrapper { - supportsUrl?: (url: URL) => boolean; - - constructor(private model: any) { - // Only define supportsUrl if the model has this method - if (typeof this.model.supportsUrl === "function") { - this.supportsUrl = (url: URL) => this.model.supportsUrl!(url); - } - } - - get specificationVersion() { - return this.model.specificationVersion; - } - - get provider(): string { - return this.model.provider; - } - - get modelId(): string { - return this.model.modelId; - } - - get defaultObjectGenerationMode() { - return this.model.defaultObjectGenerationMode; - } - - get supportsImageUrls(): boolean | undefined { - return this.model.supportsImageUrls; - } - - get supportsStructuredOutputs(): boolean | undefined { - return this.model.supportsStructuredOutputs; - } - - // For the first cut, do not support custom span_info arguments. We can - // propagate those via async local storage - async doGenerate(options: any) { - const span = startSpan( - withSpanInstrumentationName( - { - name: "Chat Completion", - spanAttributes: { - type: "llm", - }, - }, - INSTRUMENTATION_NAMES.AI_SDK, - ), - ); - const { prompt, mode, ...rest } = options; - const startTime = getCurrentUnixTimestamp(); - - try { - const ret = await this.model.doGenerate(options); - span.log({ - input: postProcessPrompt(prompt), - metadata: { - model: this.modelId, - ...rest, - ...("tools" in mode && mode.tools - ? { tools: convertTools(mode.tools) } - : "tool" in mode && mode.tool - ? { tools: convertTools([mode.tool]) } - : {}), - }, - output: postProcessOutput(ret.text, ret.toolCalls, ret.finishReason), - metrics: { - time_to_first_token: getCurrentUnixTimestamp() - startTime, - tokens: !isEmpty(ret.usage) - ? ret.usage.promptTokens + ret.usage.completionTokens - : undefined, - prompt_tokens: ret.usage?.promptTokens, - completion_tokens: ret.usage?.completionTokens, - cached: parseCachedHeader( - ret.rawResponse?.headers?.[X_CACHED_HEADER] ?? - ret.rawResponse?.headers?.[LEGACY_CACHED_HEADER], - ), - }, - }); - return ret; - } finally { - span.end(); - } - } - - async doStream(options: any) { - const { prompt, mode, ...rest } = options; - const startTime = getCurrentUnixTimestamp(); - - const span = startSpan( - withSpanInstrumentationName( - { - name: "Chat Completion", - spanAttributes: { - type: "llm", - }, - }, - INSTRUMENTATION_NAMES.AI_SDK, - ), - ); - - span.log({ - input: postProcessPrompt(prompt), - metadata: { - model: this.modelId, - ...rest, - ...("tools" in mode && mode.tools - ? { tools: convertTools(mode.tools) } - : "tool" in mode && mode.tool - ? { tools: convertTools([mode.tool]) } - : {}), - }, - }); - - let ended = false; - const end = () => { - if (!ended) { - span.end(); - ended = true; - } - }; - - try { - const ret = await this.model.doStream(options); - - let time_to_first_token: number | undefined = undefined; - let usage: - | { - promptTokens: number; - completionTokens: number; - } - | undefined = undefined; - let fullText: string | undefined = undefined; - const toolCalls: Record = {}; - let finishReason: any | undefined = undefined; - return { - ...ret, - stream: ret.stream.pipeThrough( - new TransformStream({ - transform(chunk, controller) { - if (time_to_first_token === undefined) { - time_to_first_token = getCurrentUnixTimestamp() - startTime; - span.log({ metrics: { time_to_first_token } }); - } - switch (chunk.type) { - case "text-delta": - if (fullText === undefined) { - fullText = ""; - } - fullText += chunk.textDelta; - break; - case "tool-call": - toolCalls[chunk.toolCallId] = { - toolCallType: chunk.toolCallType, - toolCallId: chunk.toolCallId, - toolName: chunk.toolName, - args: chunk.args, - }; - break; - case "tool-call-delta": - if (toolCalls[chunk.toolCallId] === undefined) { - toolCalls[chunk.toolCallId] = { - toolCallType: chunk.toolCallType, - toolCallId: chunk.toolCallId, - toolName: chunk.toolName, - args: "", - }; - } - toolCalls[chunk.toolCallId].args += chunk.argsTextDelta; - break; - case "finish": - usage = chunk.usage; - finishReason = chunk.finishReason; - break; - } - - controller.enqueue(chunk); - }, - async flush(controller) { - span.log({ - output: postProcessOutput( - fullText, - Object.keys(toolCalls).length > 0 - ? Object.values(toolCalls) - : undefined, - finishReason!, - ), - metrics: { - time_to_first_token, - tokens: !isEmpty(usage) - ? usage.promptTokens + usage.completionTokens - : undefined, - prompt_tokens: usage?.promptTokens, - completion_tokens: usage?.completionTokens, - cached: parseCachedHeader( - ret.rawResponse?.headers?.[X_CACHED_HEADER] ?? - ret.rawResponse?.headers?.[LEGACY_CACHED_HEADER], - ), - }, - }); - end(); - controller.terminate(); - }, - }), - ), - }; - } finally { - end(); - } - } -} - -function convertTools(tools: Array): any[] { - return tools.map((tool) => { - const { type: _, ...rest } = tool; - return { - type: tool.type, - function: rest, - }; - }) as any[]; -} - -function postProcessPrompt(prompt: any): any[] { - return prompt.flatMap((message: any) => { - switch (message.role) { - case "system": - return [ - { - role: "system", - content: message.content, - }, - ]; - case "assistant": - const textPart = message.content.find( - (part: any) => part.type === "text", - ) as any | undefined; - const toolCallParts = message.content.filter( - (part: any) => part.type === "tool-call", - ) as any[]; - return [ - { - role: "assistant", - content: textPart?.text, - ...(toolCallParts.length > 0 - ? { - tool_calls: toolCallParts.map((part) => ({ - id: part.toolCallId, - function: { - name: part.toolName, - arguments: JSON.stringify(part.args), - }, - type: "function" as const, - })), - } - : {}), - }, - ]; - case "user": - return [ - { - role: "user", - content: message.content.map((part: any): any => { - switch (part.type) { - case "text": - return { - type: "text", - text: part.text, - ...(part.providerMetadata - ? { providerMetadata: part.providerMetadata } - : {}), - }; - case "image": - return { - type: "image_url", - image_url: { - url: part.image.toString(), - ...(part.providerMetadata - ? { providerMetadata: part.providerMetadata } - : {}), - }, - }; - default: - // We don't support files directly but also don't want to block them from being logged - return part as any; - } - }), - }, - ]; - case "tool": - return message.content.map((part: any) => ({ - role: "tool", - tool_call_id: part.toolCallId, - content: JSON.stringify(part.result), - })); - } - }); -} - -function postProcessOutput( - text: string | undefined, - toolCalls: any[] | undefined, - finishReason: any, -) { - return [ - { - index: 0, - message: { - role: "assistant", - content: text ?? "", - ...(toolCalls && toolCalls.length > 0 - ? { - tool_calls: toolCalls.map((toolCall) => ({ - id: toolCall.toolCallId, - function: { - name: toolCall.toolName, - arguments: toolCall.args, - }, - type: "function" as const, - })), - } - : {}), - }, - finish_reason: finishReason, - }, - ]; -} diff --git a/js/src/wrappers/ai-sdk/harness-agent-context.ts b/js/src/wrappers/ai-sdk/harness-agent-context.ts index f17f2a442..0aa4fbb5d 100644 --- a/js/src/wrappers/ai-sdk/harness-agent-context.ts +++ b/js/src/wrappers/ai-sdk/harness-agent-context.ts @@ -2,8 +2,8 @@ import iso from "../../isomorph"; import type { IsoAsyncLocalStorage, IsoTracingChannel } from "../../isomorph"; import { _internalGetGlobalState, + _internalStartSpan, currentSpan, - startSpan, updateSpan, type Span, type StartSpanArgs, @@ -359,9 +359,10 @@ export function startHarnessTurnChildSpan( args, INSTRUMENTATION_NAMES.AI_SDK, ); + const { parent: _ignoredParent, ...publicSpanArgs } = spanArgs; return typeof parent === "string" - ? startSpan({ ...spanArgs, parent }) - : parent.startSpan(spanArgs); + ? _internalStartSpan({ ...spanArgs, parent }) + : parent.startSpan(publicSpanArgs); } export function updateHarnessTurn( diff --git a/js/src/wrappers/ai-sdk/index.ts b/js/src/wrappers/ai-sdk/index.ts index b2058cd16..f9e1d86f3 100644 --- a/js/src/wrappers/ai-sdk/index.ts +++ b/js/src/wrappers/ai-sdk/index.ts @@ -1,6 +1,2 @@ export { wrapAISDK, wrapAgentClass } from "./ai-sdk"; export { braintrustAISDKTelemetry } from "./telemetry"; - -// TODO: remove in the next major release -export { wrapAISDKModel } from "./deprecated/wrapAISDKModel"; -export { BraintrustMiddleware } from "./deprecated/BraintrustMiddleware"; diff --git a/js/src/wrappers/exports.test.ts b/js/src/wrappers/exports.test.ts deleted file mode 100644 index 66850a3b3..000000000 --- a/js/src/wrappers/exports.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { BraintrustMiddleware } from "./ai-sdk"; - -describe("ai-sdk exports", () => { - it("should always export BraintrustMiddleware as a function", () => { - expect(typeof BraintrustMiddleware).toBe("function"); - }); - - it("BraintrustMiddleware should return an object with wrapGenerate and wrapStream", () => { - const result = BraintrustMiddleware({}); - expect(result).toHaveProperty("wrapGenerate"); - expect(result).toHaveProperty("wrapStream"); - expect(typeof result.wrapGenerate).toBe("function"); - expect(typeof result.wrapStream).toBe("function"); - }); - - it("should handle conditional imports gracefully", () => { - // Test that imports don't throw errors regardless of AI SDK version - expect(() => { - const middleware = BraintrustMiddleware({ debug: true }); - - // Should be able to call the functions without errors - const { wrapGenerate, wrapStream } = middleware; - - expect(wrapGenerate).toBeDefined(); - expect(wrapStream).toBeDefined(); - }).not.toThrow(); - }); - - it("should export middleware functions that can be instantiated", () => { - const middleware = BraintrustMiddleware({}); - const { wrapGenerate, wrapStream } = middleware; - - // Should be functions that can be called (we don't test actual execution due to logger dependencies) - expect(typeof wrapGenerate).toBe("function"); - expect(typeof wrapStream).toBe("function"); - }); -}); diff --git a/js/src/wrappers/langchain/callback-handler.ts b/js/src/wrappers/langchain/callback-handler.ts index 51b073173..27bf90f28 100644 --- a/js/src/wrappers/langchain/callback-handler.ts +++ b/js/src/wrappers/langchain/callback-handler.ts @@ -89,13 +89,14 @@ export class BraintrustLangChainCallbackHandler { INSTRUMENTATION_NAMES.LANGCHAIN, ); - let span = parentSpan.startSpan(spanArgs); + const { parent: _ignoredParent, ...publicSpanArgs } = spanArgs; + let span = parentSpan.startSpan(publicSpanArgs); if ( !Object.is(this.options.logger, NOOP_SPAN) && Object.is(span, NOOP_SPAN) ) { - span = initLogger().startSpan(spanArgs); + span = initLogger().startSpan(publicSpanArgs); } this.spans.set(runId, span); diff --git a/js/src/wrappers/mastra.ts b/js/src/wrappers/mastra.ts index b3ff3b419..4ecaaacbe 100644 --- a/js/src/wrappers/mastra.ts +++ b/js/src/wrappers/mastra.ts @@ -75,7 +75,7 @@ interface MastraTracingEvent { } /** Subset of the `ObservabilityExporter` contract from `@mastra/core`. */ -export interface MastraObservabilityExporter { +interface MastraObservabilityExporter { name: string; init?(options: unknown): void; __setLogger?(logger: unknown): void; @@ -328,11 +328,12 @@ export class BraintrustObservabilityExporter implements MastraObservabilityExpor } } + const { parent: _ignoredParent, ...publicArgs } = args; const span = parentRecord - ? parentRecord.span.startSpan(args) + ? parentRecord.span.startSpan(publicArgs) : this.capturedParent - ? this.capturedParent.startSpan(args) - : startSpan(args); + ? this.capturedParent.startSpan(publicArgs) + : startSpan(publicArgs); const record: SpanRecord = { span, hasLoggedInput: false }; this.logPayload(record, exported); @@ -408,40 +409,3 @@ export class BraintrustObservabilityExporter implements MastraObservabilityExpor function logExporterError(err: unknown): void { debugLogger.warn("Mastra exporter failure:", err); } - -/** - * @deprecated Mastra is now instrumented through its own `ObservabilityExporter` - * contract instead of by wrapping the agent. This function does nothing and - * will be removed in the next major release. - * - * To capture Mastra spans in Braintrust, do one of: - * - * - **Auto-instrumentation**: run your app with - * `node --import braintrust/hook.mjs`. The loader installs - * `BraintrustObservabilityExporter` into every `new Mastra(...)` - * automatically. - * - **Manual wiring**: pass the exporter yourself: - * - * ```ts - * import { Mastra } from "@mastra/core"; - * import { Observability } from "@mastra/observability"; - * import { BraintrustObservabilityExporter } from "braintrust"; - * - * const mastra = new Mastra({ - * observability: new Observability({ - * configs: { - * default: { - * serviceName: "my-service", - * exporters: [new BraintrustObservabilityExporter()], - * }, - * }, - * }), - * }); - * ``` - */ -export function wrapMastraAgent( - agent: T, - _options?: { name?: string; span_name?: string }, -): T { - return agent; -} diff --git a/js/src/wrappers/node-test/suite.ts b/js/src/wrappers/node-test/suite.ts index c2ee794df..80cf58dae 100644 --- a/js/src/wrappers/node-test/suite.ts +++ b/js/src/wrappers/node-test/suite.ts @@ -1,4 +1,4 @@ -import { initExperiment, type Experiment } from "../../logger"; +import { init, type Experiment } from "../../logger"; import { runTracedEval } from "../shared/traced-eval"; import { summarizeAndFlush } from "../shared/flush"; import type { @@ -40,7 +40,8 @@ export function initNodeTestSuite(config: NodeTestSuiteConfig): NodeTestSuite { const experimentName = config.experimentName || `${config.projectName}-${new Date().toISOString()}`; - experiment = initExperiment(config.projectName, { + experiment = init({ + project: config.projectName, experiment: experimentName, }); return experiment; diff --git a/js/src/wrappers/node-test/test-helpers.ts b/js/src/wrappers/node-test/test-helpers.ts index 0047bf5ac..a455eae66 100644 --- a/js/src/wrappers/node-test/test-helpers.ts +++ b/js/src/wrappers/node-test/test-helpers.ts @@ -19,14 +19,12 @@ export async function setupNodeTestEnv(): Promise { await _exportsForTestingOnly.simulateLoginForTests(); const bgLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - vi.spyOn(logger, "initExperiment").mockImplementation( - (projectName: string, options?: any) => { - return _exportsForTestingOnly.initTestExperiment( - options?.experiment || "test-experiment", - projectName, - ); - }, - ); + vi.spyOn(logger, "init").mockImplementation((options) => { + return _exportsForTestingOnly.initTestExperiment( + options.experiment || "test-experiment", + options.project ?? options.projectId ?? "test-project", + ); + }); return bgLogger; } diff --git a/js/src/wrappers/oai.ts b/js/src/wrappers/oai.ts index 743460b7e..0599787e0 100644 --- a/js/src/wrappers/oai.ts +++ b/js/src/wrappers/oai.ts @@ -1,10 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { CompiledPrompt } from "../logger"; -import { - LEGACY_CACHED_HEADER, - parseCachedHeader, - X_CACHED_HEADER, -} from "../openai-utils"; import { responsesProxy } from "./oai_responses"; import type { ArgsOf, @@ -72,7 +67,7 @@ globalThis.__inherited_braintrust_wrap_openai = wrapOpenAI; type OpenAILike = OpenAIV4Client; -export function wrapOpenAIv4(openai: T): T { +function wrapOpenAIv4(openai: T): T { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const typedOpenai = openai as OpenAIV4Client; // Recover `this` for fallback methods so private fields and internal slots @@ -238,8 +233,6 @@ function wrapBetaChatCompletionStream

( }; } -export { LEGACY_CACHED_HEADER, parseCachedHeader, X_CACHED_HEADER }; - function wrapChatCompletion< P extends OpenAIChatCreateParams, C extends OpenAIChatCompletion | OpenAIChatStream, diff --git a/js/src/wrappers/vitest-evals/reporter.test.ts b/js/src/wrappers/vitest-evals/reporter.test.ts index 7b7385306..ccff70440 100644 --- a/js/src/wrappers/vitest-evals/reporter.test.ts +++ b/js/src/wrappers/vitest-evals/reporter.test.ts @@ -16,7 +16,7 @@ beforeAll(async () => { await _exportsForTestingOnly.simulateLoginForTests(); backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - vi.spyOn(logger, "initExperiment").mockImplementation( + vi.spyOn(logger, "init").mockImplementation( (projectOrOptions: string | any, options?: any) => { const experimentOptions = typeof projectOrOptions === "string" ? options : projectOrOptions; diff --git a/js/src/wrappers/vitest-evals/reporter.ts b/js/src/wrappers/vitest-evals/reporter.ts index 71ba251e3..ad3de38be 100644 --- a/js/src/wrappers/vitest-evals/reporter.ts +++ b/js/src/wrappers/vitest-evals/reporter.ts @@ -1,11 +1,6 @@ import type { Reporter, TestCase, TestModule, Vitest } from "vitest/node"; import { SpanTypeAttribute, isObject } from "../../../util"; -import { - initExperiment, - logError, - type Experiment, - type Span, -} from "../../logger"; +import { init, logError, type Experiment, type Span } from "../../logger"; import { configureNode } from "../../node/config"; import { summarizeAndFlush } from "../shared/flush"; @@ -202,7 +197,7 @@ export default class BraintrustVitestEvalsReporter implements Reporter { const experimentName = this.options.experimentName ?? `vitest-evals-${new Date().toISOString()}`; - this.experiment = initExperiment({ + this.experiment = init({ ...(projectId ? { projectId } : { project: projectName }), experiment: experimentName, metadata: this.options.metadata, diff --git a/js/src/wrappers/vitest/context-manager.ts b/js/src/wrappers/vitest/context-manager.ts index 03cb41799..88b6198b3 100644 --- a/js/src/wrappers/vitest/context-manager.ts +++ b/js/src/wrappers/vitest/context-manager.ts @@ -12,7 +12,7 @@ import type { Dataset, Experiment } from "../../logger"; * - flushPromise/flushResolved: Coordination for experiment flushing */ export interface VitestExperimentContext { - dataset: Dataset | undefined; + dataset: Dataset | undefined; experiment: Experiment; datasetExamples: Map; parent?: VitestExperimentContext; // Link to parent describe @@ -65,7 +65,7 @@ class VitestContextManager { } createChildContext( - dataset: Dataset | undefined, + dataset: Dataset | undefined, experiment: Experiment, ): VitestExperimentContext { const parent = this.getCurrentContext(); diff --git a/js/src/wrappers/vitest/vitest-wrapper-example.test.ts b/js/src/wrappers/vitest/vitest-wrapper-example.test.ts index c646c0fdf..452e6e8ca 100644 --- a/js/src/wrappers/vitest/vitest-wrapper-example.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper-example.test.ts @@ -11,7 +11,7 @@ await _exportsForTestingOnly.simulateLoginForTests(); const moduleBackgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); vitest.vi - .spyOn(logger, "initExperiment") + .spyOn(logger, "init") .mockImplementation((projectName: string, options?: any) => { return _exportsForTestingOnly.initTestExperiment( options?.experiment || "test-experiment", diff --git a/js/src/wrappers/vitest/vitest-wrapper-span.test.ts b/js/src/wrappers/vitest/vitest-wrapper-span.test.ts index 47cbcbdbe..a7995b20a 100644 --- a/js/src/wrappers/vitest/vitest-wrapper-span.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper-span.test.ts @@ -11,7 +11,7 @@ await _exportsForTestingOnly.simulateLoginForTests(); const moduleBackgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); vitest.vi - .spyOn(logger, "initExperiment") + .spyOn(logger, "init") .mockImplementation((projectName: string, options?: any) => { return _exportsForTestingOnly.initTestExperiment( options?.experiment || "test-experiment", diff --git a/js/src/wrappers/vitest/vitest-wrapper.test.ts b/js/src/wrappers/vitest/vitest-wrapper.test.ts index 6154b21b0..d3d7bed6f 100644 --- a/js/src/wrappers/vitest/vitest-wrapper.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper.test.ts @@ -32,13 +32,13 @@ _exportsForTestingOnly.setInitialTestState(); await _exportsForTestingOnly.simulateLoginForTests(); _exportsForTestingOnly.useTestBackgroundLogger(); -// ✅ STEP 2: Mock initDataset and initExperiment to avoid network calls +// ✅ STEP 2: Mock initDataset and init to avoid network calls vi.spyOn(logger, "initDataset").mockReturnValue({ insert: vi.fn(() => "test-example-id"), } as any); -const initExperimentSpy = vi - .spyOn(logger, "initExperiment") +const initSpy = vi + .spyOn(logger, "init") .mockImplementation((projectOrOptions: string | any, options?: any) => { const experimentOptions = typeof projectOrOptions === "string" ? options : projectOrOptions; @@ -451,11 +451,11 @@ describe("Project selection", () => { } beforeEach(() => { - initExperimentSpy.mockClear(); + initSpy.mockClear(); _resetContextManager(); }); - test("wrapVitest passes projectId to initExperiment", async () => { + test("wrapVitest passes projectId to init", async () => { const bt = wrapVitest(makeFakeVitestMethods(), { projectId: "project-id-123", displaySummary: false, @@ -465,7 +465,7 @@ describe("Project selection", () => { bt.test("uses project id", async () => undefined); }); - expect(initExperimentSpy).toHaveBeenCalledWith( + expect(initSpy).toHaveBeenCalledWith( expect.objectContaining({ projectId: "project-id-123", experiment: expect.stringMatching(/^project id suite-/), @@ -484,15 +484,12 @@ describe("Project selection", () => { bt.test("uses project id", async () => undefined); }); - expect(initExperimentSpy).toHaveBeenCalledWith( + expect(initSpy).toHaveBeenCalledWith( expect.objectContaining({ projectId: "project-id-123", }), ); - expect(initExperimentSpy).not.toHaveBeenCalledWith( - "project-name", - expect.anything(), - ); + expect(initSpy).not.toHaveBeenCalledWith("project-name", expect.anything()); }); test("wrapVitest preserves projectName behavior when projectId is absent", async () => { @@ -505,9 +502,9 @@ describe("Project selection", () => { bt.test("uses project name", async () => undefined); }); - expect(initExperimentSpy).toHaveBeenCalledWith( - "project-name", + expect(initSpy).toHaveBeenCalledWith( expect.objectContaining({ + project: "project-name", experiment: expect.stringMatching(/^project name suite-/), }), ); @@ -522,9 +519,9 @@ describe("Project selection", () => { bt.test("uses suite name", async () => undefined); }); - expect(initExperimentSpy).toHaveBeenCalledWith( - "fallback suite", + expect(initSpy).toHaveBeenCalledWith( expect.objectContaining({ + project: "fallback suite", experiment: expect.stringMatching(/^fallback suite-/), }), ); diff --git a/js/src/wrappers/vitest/wrapper.ts b/js/src/wrappers/vitest/wrapper.ts index 4be2adf5b..c0f0ee381 100644 --- a/js/src/wrappers/vitest/wrapper.ts +++ b/js/src/wrappers/vitest/wrapper.ts @@ -1,4 +1,4 @@ -import { initExperiment } from "../../logger"; +import { init } from "../../logger"; import type { TestConfig, TestContext, @@ -215,11 +215,12 @@ export function wrapDescribe( const experimentName = `${suiteName}-${new Date().toISOString()}`; const experiment = config.projectId - ? initExperiment({ + ? init({ projectId: config.projectId, experiment: experimentName, }) - : initExperiment(config.projectName || suiteName, { + : init({ + project: config.projectName || suiteName, experiment: experimentName, }); diff --git a/js/tests/api-compatibility/api-compatibility.test.ts b/js/tests/api-compatibility/api-compatibility.test.ts index 3941302be..6241d23d6 100644 --- a/js/tests/api-compatibility/api-compatibility.test.ts +++ b/js/tests/api-compatibility/api-compatibility.test.ts @@ -20,7 +20,7 @@ import type { Options } from "tsup"; * ## How It Works * * 1. Downloads the latest published version from npm - * 2. Extracts the .d.ts files for each entrypoint (main, browser, util) + * 2. Extracts the .d.ts files for each built entrypoint * 3. Parses both published and current .d.ts files using TypeScript Compiler API * 4. Compares exported symbols (functions, classes, interfaces, types, etc.) * 5. Fails if breaking changes are detected in non-major version bumps @@ -77,10 +77,10 @@ async function getEntrypointsFromTsupConfig(): Promise< entrypoints.push({ name, typesPath }); } } else if (typeof entry === "object") { - // entry is a record like { main: 'src/index.ts' } - for (const [key, entryFile] of Object.entries(entry)) { + // Object entry keys determine the emitted path, including nested paths. + for (const key of Object.keys(entry)) { const name = key; - const typesPath = getTypesPath(String(entryFile), outDir); + const typesPath = path.join(outDir, `${key}.d.ts`); entrypoints.push({ name, typesPath }); } } @@ -97,10 +97,8 @@ function getEntrypointName(entryFile: string, outDir: string): string { // Map common patterns to friendly names if (entryFile.includes("src/node/index.ts")) return "main"; - if (entryFile.includes("src/browser/index.ts")) return "browser"; if (entryFile.includes("src/edge-light/index.ts")) return "edge-light"; if (entryFile.includes("src/workerd/index.ts")) return "workerd"; - if (entryFile.includes("util/index.ts")) return "util"; // Default to basename return basename; @@ -2951,13 +2949,14 @@ describe("API Compatibility", () => { test("keeps public declarations free of expanded Zod schema graphs", () => { const declarationRoot = path.join(__dirname, "..", ".."); const publicDeclarationPaths = [ - "dist/index.d.ts", - "dist/browser.d.ts", - "util/dist/index.d.ts", + ...new Set(ENTRYPOINTS.map(({ typesPath }) => typesPath)), ]; for (const declarationTypesPath of publicDeclarationPaths) { const declarationPath = path.join(declarationRoot, declarationTypesPath); + if (!fs.existsSync(declarationPath)) { + continue; + } const declaration = fs.readFileSync(declarationPath, "utf8"); expect(declaration).not.toMatch(/z\.infer { "promptDefinitionSchema", "promptDefinitionWithToolsSchema", ]) { - expect(mainDeclaration).toMatch( - new RegExp(`declare const ${schemaName}: z\\.ZodType<`), - ); - } - - const utilDeclaration = fs.readFileSync( - path.join(declarationRoot, "util/dist/index.d.ts"), - "utf8", - ); - for (const schemaName of [ - "spanComponentsV3Schema", - "spanComponentsV4Schema", - "spanObjectTypeV3EnumSchema", - ]) { - expect(utilDeclaration).toMatch( - new RegExp(`declare const ${schemaName}: z\\.ZodType<`), + expect(mainDeclaration).not.toMatch( + new RegExp(`(?:declare|export) const ${schemaName}(?::|\\s*=)`), ); } }); diff --git a/js/tests/auto-instrumentations/error-handling.test.ts b/js/tests/auto-instrumentations/error-handling.test.ts index 2c31588a4..7bbccfe75 100644 --- a/js/tests/auto-instrumentations/error-handling.test.ts +++ b/js/tests/auto-instrumentations/error-handling.test.ts @@ -73,7 +73,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -82,7 +82,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -133,7 +133,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -142,7 +142,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -184,7 +184,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -193,7 +193,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -228,7 +228,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -237,7 +237,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -281,7 +281,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -290,7 +290,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -336,7 +336,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -345,7 +345,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -400,7 +400,7 @@ describe("Error Handling", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -409,7 +409,7 @@ describe("Error Handling", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/event-content.test.ts b/js/tests/auto-instrumentations/event-content.test.ts index 5a2fdd100..81acdee76 100644 --- a/js/tests/auto-instrumentations/event-content.test.ts +++ b/js/tests/auto-instrumentations/event-content.test.ts @@ -75,7 +75,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -84,7 +84,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -149,7 +149,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -158,7 +158,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -216,7 +216,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -225,7 +225,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -296,7 +296,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -305,7 +305,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -357,7 +357,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -366,7 +366,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -411,7 +411,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -420,7 +420,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -467,7 +467,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -476,7 +476,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -525,7 +525,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -534,7 +534,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -652,7 +652,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -661,7 +661,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -727,7 +727,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -736,7 +736,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -831,7 +831,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -840,7 +840,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -938,7 +938,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -947,7 +947,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1045,7 +1045,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1054,7 +1054,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1132,7 +1132,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1141,7 +1141,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1233,7 +1233,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1242,7 +1242,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1340,7 +1340,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1349,7 +1349,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1416,7 +1416,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1425,7 +1425,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1514,7 +1514,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1523,7 +1523,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1603,7 +1603,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1612,7 +1612,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -1699,7 +1699,7 @@ describe("Event Content Validation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -1708,7 +1708,7 @@ describe("Event Content Validation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/function-behavior.test.ts b/js/tests/auto-instrumentations/function-behavior.test.ts index 1797252b5..0881a6d53 100644 --- a/js/tests/auto-instrumentations/function-behavior.test.ts +++ b/js/tests/auto-instrumentations/function-behavior.test.ts @@ -63,7 +63,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -72,7 +72,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -131,7 +131,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -140,7 +140,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -178,7 +178,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -187,7 +187,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -230,7 +230,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -239,7 +239,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -284,7 +284,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -293,7 +293,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -338,7 +338,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -347,7 +347,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -392,7 +392,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -401,7 +401,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -438,7 +438,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -447,7 +447,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -489,7 +489,7 @@ describe("Function Behavior Preservation", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -498,7 +498,7 @@ describe("Function Behavior Preservation", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/multiple-instrumentations.test.ts b/js/tests/auto-instrumentations/multiple-instrumentations.test.ts index 2b48ff0a6..bb53cbe60 100644 --- a/js/tests/auto-instrumentations/multiple-instrumentations.test.ts +++ b/js/tests/auto-instrumentations/multiple-instrumentations.test.ts @@ -101,7 +101,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -110,7 +110,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -169,7 +169,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -178,7 +178,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -234,7 +234,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -243,7 +243,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -314,7 +314,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -323,7 +323,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -402,7 +402,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); // Add custom instrumentation config @@ -427,7 +427,7 @@ describe("Multiple Instrumentations", () => { outfile, format: "esm", plugins: [ - esbuildPlugin({ + braintrustEsbuildPlugin({ browser: false, instrumentations: [customConfig], }), @@ -488,7 +488,7 @@ describe("Multiple Instrumentations", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -497,7 +497,7 @@ describe("Multiple Instrumentations", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/next-config.test.ts b/js/tests/auto-instrumentations/next-config.test.ts index 91d96d401..7ebefe3ce 100644 --- a/js/tests/auto-instrumentations/next-config.test.ts +++ b/js/tests/auto-instrumentations/next-config.test.ts @@ -1,14 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../../src/auto-instrumentations/bundler/webpack.js", () => ({ - webpackPlugin: vi.fn((options: unknown) => ({ + braintrustWebpackPlugin: vi.fn((options: unknown) => ({ apply: () => {}, name: "braintrust-test-webpack-plugin", options, })), })); -import { webpackPlugin } from "../../src/auto-instrumentations/bundler/webpack.js"; +import { braintrustWebpackPlugin } from "../../src/auto-instrumentations/bundler/webpack.js"; import { wrapNextjsConfigWithBraintrust } from "../../src/auto-instrumentations/bundler/next.js"; const originalArgv = [...process.argv]; @@ -45,7 +45,7 @@ describe("wrapNextjsConfigWithBraintrust", () => { const result = config.webpack({ plugins: [] }, { isServer: false }); expect(userWebpack).toHaveBeenCalledOnce(); - expect(webpackPlugin).toHaveBeenCalledWith({ browser: true }); + expect(braintrustWebpackPlugin).toHaveBeenCalledWith({ browser: true }); expect(result.plugins).toHaveLength(2); expect(result.plugins[0]).toEqual({ name: "user-plugin" }); expect(result.plugins[1].options).toEqual({ browser: true }); @@ -183,8 +183,8 @@ describe("wrapNextjsConfigWithBraintrust", () => { }, { resolve: (specifier: string) => { - if (specifier === "braintrust/webpack-loader") { - return "/braintrust/webpack-loader.cjs"; + if (specifier === "braintrust/package.json") { + return "/braintrust/package.json"; } throw new Error(`Cannot resolve module ${specifier}`); diff --git a/js/tests/auto-instrumentations/runtime-execution.test.ts b/js/tests/auto-instrumentations/runtime-execution.test.ts index eb882dd3f..991826798 100644 --- a/js/tests/auto-instrumentations/runtime-execution.test.ts +++ b/js/tests/auto-instrumentations/runtime-execution.test.ts @@ -63,7 +63,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -72,7 +72,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -122,7 +122,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -131,7 +131,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -188,7 +188,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -197,7 +197,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -233,7 +233,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { vitePlugin } = + const { braintrustVitePlugin } = await import("../../src/auto-instrumentations/bundler/vite.js"); await viteBuild({ @@ -248,7 +248,7 @@ describe("Runtime Execution of Bundled Code", () => { emptyOutDir: true, minify: false, }, - plugins: [vitePlugin({ browser: false })], + plugins: [braintrustVitePlugin({ browser: false })], logLevel: "error", resolve: { preserveSymlinks: true, @@ -292,7 +292,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { vitePlugin } = + const { braintrustVitePlugin } = await import("../../src/auto-instrumentations/bundler/vite.js"); await viteBuild({ @@ -307,7 +307,7 @@ describe("Runtime Execution of Bundled Code", () => { emptyOutDir: true, minify: false, }, - plugins: [vitePlugin({ browser: false })], + plugins: [braintrustVitePlugin({ browser: false })], logLevel: "error", resolve: { preserveSymlinks: true, @@ -344,7 +344,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { rollupPlugin } = + const { braintrustRollupPlugin } = await import("../../src/auto-instrumentations/bundler/rollup.js"); // Simple resolver plugin @@ -363,7 +363,7 @@ describe("Runtime Execution of Bundled Code", () => { const bundle = await rollup({ input: entryPoint, - plugins: [resolverPlugin, rollupPlugin({ browser: false })], + plugins: [resolverPlugin, braintrustRollupPlugin({ browser: false })], external: [], preserveSymlinks: true, }); @@ -413,7 +413,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { rollupPlugin } = + const { braintrustRollupPlugin } = await import("../../src/auto-instrumentations/bundler/rollup.js"); const resolverPlugin = { @@ -431,7 +431,7 @@ describe("Runtime Execution of Bundled Code", () => { const bundle = await rollup({ input: entryPoint, - plugins: [resolverPlugin, rollupPlugin({ browser: false })], + plugins: [resolverPlugin, braintrustRollupPlugin({ browser: false })], external: [], preserveSymlinks: true, }); @@ -476,7 +476,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -485,7 +485,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -530,7 +530,7 @@ describe("Runtime Execution of Bundled Code", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -539,7 +539,7 @@ describe("Runtime Execution of Bundled Code", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/streaming-and-responses.test.ts b/js/tests/auto-instrumentations/streaming-and-responses.test.ts index e994de00e..aafd66bf5 100644 --- a/js/tests/auto-instrumentations/streaming-and-responses.test.ts +++ b/js/tests/auto-instrumentations/streaming-and-responses.test.ts @@ -155,7 +155,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -164,7 +164,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -216,7 +216,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -225,7 +225,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -310,7 +310,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -319,7 +319,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -400,7 +400,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -409,7 +409,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -461,7 +461,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -470,7 +470,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, @@ -554,7 +554,7 @@ describe("Streaming Methods and Responses API", () => { fs.writeFileSync(entryPoint, testCode); - const { esbuildPlugin } = + const { braintrustEsbuildPlugin } = await import("../../src/auto-instrumentations/bundler/esbuild.js"); await esbuild.build({ @@ -563,7 +563,7 @@ describe("Streaming Methods and Responses API", () => { write: true, outfile, format: "esm", - plugins: [esbuildPlugin({ browser: false })], + plugins: [braintrustEsbuildPlugin({ browser: false })], logLevel: "error", absWorkingDir: fixturesDir, preserveSymlinks: true, diff --git a/js/tests/auto-instrumentations/transformation.test.ts b/js/tests/auto-instrumentations/transformation.test.ts index 0a21456cd..18800bdfd 100644 --- a/js/tests/auto-instrumentations/transformation.test.ts +++ b/js/tests/auto-instrumentations/transformation.test.ts @@ -630,7 +630,7 @@ describe("Orchestrion Transformation Tests", () => { it.each([ ["browser", "browser", { browser: true }], - ["legacy-browser", "browser", { useDiagnosticChannelCompatShim: true }], + ["legacy-browser", "browser", { browser: true }], ["edge", "neutral", { browser: true }], ] as const)( "should keep Mastra %s bundles free of Node-only patches", @@ -933,13 +933,7 @@ describe("Orchestrion Transformation Tests", () => { }); it.each([ - [ - "skip", - "legacy browser", - "web", - { useDiagnosticChannelCompatShim: true }, - false, - ], + ["skip", "legacy browser", "web", { browser: true }, false], ["skip", "browser", "web", { browser: true }, false], ["apply", "node", "node", { browser: false }, true], ] as const)( diff --git a/js/tsup.config.ts b/js/tsup.config.ts index 416ef9679..760f06fbf 100644 --- a/js/tsup.config.ts +++ b/js/tsup.config.ts @@ -14,6 +14,7 @@ export default defineConfig([ { entry: { index: "src/node/index.ts", + "instrumentation/index": "src/instrumentation/index.ts", "apply-auto-instrumentation": "src/node/apply-auto-instrumentation-entry.ts", "vitest-evals-reporter": "src/wrappers/vitest-evals/reporter.ts", @@ -32,22 +33,6 @@ export default defineConfig([ define, clean: true, }, - { - entry: ["util/index.ts"], - format: ["cjs", "esm"], - outDir: "util/dist", - external: ["esbuild", "prettier", "typescript", "zod"], - removeNodeProtocol: false, - dts: { - // Split DTS generation to reduce memory usage - compilerOptions: { - skipLibCheck: true, - }, - }, - splitting: true, - define, - clean: true, - }, // Browser/edge entrypoints { entry: { @@ -67,20 +52,6 @@ export default defineConfig([ define, clean: false, }, - { - entry: ["src/instrumentation/index.ts"], - format: ["cjs", "esm"], - outDir: "dist/instrumentation", - external: ["@braintrust/instrumentation-core", "zod"], - dts: { - compilerOptions: { - skipLibCheck: true, - }, - }, - splitting: false, - define, - clean: true, - }, { entry: [ "src/auto-instrumentations/index.ts", diff --git a/js/util/object.ts b/js/util/object.ts index 941c6264a..074aa6229 100644 --- a/js/util/object.ts +++ b/js/util/object.ts @@ -23,7 +23,6 @@ export type OtherExperimentLogFields = { classifications?: Record; metadata: Record; metrics: Record; - datasetRecordId: string; origin: ObjectReferenceType; span_attributes: Record; [ASYNC_SCORING_CONTROL_FIELD]: AsyncScoringControl; @@ -43,7 +42,7 @@ export type ExperimentLogFullArgs = Partial< export type LogFeedbackFullArgs = IdField & Partial< - Omit & { + Omit & { comment: string; source: Source; } diff --git a/js/util/score.ts b/js/util/score.ts index 1c8a4754e..53da7063a 100644 --- a/js/util/score.ts +++ b/js/util/score.ts @@ -40,11 +40,6 @@ export interface Score { name: string; score: number | null; metadata?: Record; - // DEPRECATION_NOTICE: this field is deprecated, as errors are propagated up to the caller. - /** - * @deprecated - */ - error?: unknown; } export type ScorerArgs = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6deed80a..2e0e646d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ importers: e2e: devDependencies: + '@braintrust/browser': + specifier: workspace:^ + version: link:../integrations/browser-js '@braintrust/langchain-js': specifier: workspace:^ version: link:../integrations/langchain-js From c4d05336cc40aa321bc2b0393ea76b44d76be026 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 14 Sep 2026 15:35:31 +0200 Subject: [PATCH 09/19] ref(ai-sdk)!: Remove AI SDK v3 support (#2474) --- .changeset/remove-ai-sdk-v3.md | 5 + e2e/README.md | 2 +- e2e/config/pr-comment-scenarios.json | 8 - .../ai-sdk-v3-latest.cassette.json | 1148 ---------------- .../__cassettes__/ai-sdk-v3.cassette.json | 1151 ----------------- .../ai-sdk-v3-auto-hook.span-tree.json | 918 ------------- .../ai-sdk-v3-auto-hook.span-tree.txt | 833 ------------ .../ai-sdk-v3-latest-auto-hook.span-tree.json | 918 ------------- .../ai-sdk-v3-latest-auto-hook.span-tree.txt | 833 ------------ .../ai-sdk-v3-latest-wrapped.span-tree.json | 918 ------------- .../ai-sdk-v3-latest-wrapped.span-tree.txt | 833 ------------ .../ai-sdk-v3-wrapped.span-tree.json | 918 ------------- .../ai-sdk-v3-wrapped.span-tree.txt | 833 ------------ .../ai-sdk-instrumentation/package.json | 12 - .../ai-sdk-instrumentation/pnpm-lock.yaml | 574 -------- .../scenario.ai-sdk-v3.mjs | 21 - .../scenario.ai-sdk-v3.ts | 30 - .../ai-sdk-instrumentation/scenario.impl.mjs | 32 - integrations/val.town/deno.json | 3 +- integrations/val.town/deno.lock | 1102 ++++++++-------- integrations/val.town/vals/tutorial/code.ts | 2 +- js/package.json | 1 - js/scripts/pull-vendor.sh | 4 +- .../auto-instrumentations/configs/ai-sdk.ts | 244 +--- .../providers/ai-sdk-channels.ts | 18 - .../ai-sdk-instrumentation.streaming.test.ts | 22 +- .../providers/ai-sdk-instrumentation.test.ts | 57 +- .../providers/ai-sdk-instrumentation.ts | 61 +- .../cloudflare-think-instrumentation.ts | 24 +- js/src/vendor-sdk-types/ai-sdk-common.ts | 3 + js/src/vendor-sdk-types/ai-sdk-v3.ts | 12 - js/src/vendor-sdk-types/ai-sdk-v4.ts | 3 - js/src/vendor-sdk-types/ai-sdk-v5.ts | 6 - js/src/vendor-sdk-types/ai-sdk-v6.ts | 7 - js/src/vendor-sdk-types/ai-sdk.ts | 14 +- js/src/wrappers/ai-sdk/ai-sdk.ts | 12 +- 36 files changed, 677 insertions(+), 10905 deletions(-) create mode 100644 .changeset/remove-ai-sdk-v3.md delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__cassettes__/ai-sdk-v3-latest.cassette.json delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__cassettes__/ai-sdk-v3.cassette.json delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-auto-hook.span-tree.json delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-auto-hook.span-tree.txt delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-auto-hook.span-tree.json delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-auto-hook.span-tree.txt delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-wrapped.span-tree.json delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-wrapped.span-tree.txt delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-wrapped.span-tree.json delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-wrapped.span-tree.txt delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/scenario.ai-sdk-v3.mjs delete mode 100644 e2e/scenarios/ai-sdk-instrumentation/scenario.ai-sdk-v3.ts delete mode 100644 js/src/vendor-sdk-types/ai-sdk-v3.ts delete mode 100644 js/src/vendor-sdk-types/ai-sdk-v4.ts delete mode 100644 js/src/vendor-sdk-types/ai-sdk-v5.ts delete mode 100644 js/src/vendor-sdk-types/ai-sdk-v6.ts diff --git a/.changeset/remove-ai-sdk-v3.md b/.changeset/remove-ai-sdk-v3.md new file mode 100644 index 000000000..987c8fed2 --- /dev/null +++ b/.changeset/remove-ai-sdk-v3.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref(ai-sdk)!: Remove AI SDK v3 support diff --git a/e2e/README.md b/e2e/README.md index d9ac582ea..47bd2b00f 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -80,7 +80,7 @@ Wrapper scenarios often create a root span with `testRunId` metadata and then le - Find the scenario root span first. - Scope raw payload snapshots by `root_span_id` using `payloadRowsForRootSpan(...)`. - Prefer normalized span-tree snapshots from `matchSpanTreeSnapshot(...)`. The `.json` sibling is the structural contract, and the `.txt` sibling is the ASCII tree for review; both are asserted and should be updated together. -- If the wrapper has an explicit support matrix, reuse one shared test across version-specific scenario entries instead of duplicating the assertions. The AI SDK wrapper scenario uses this for supported v3-v6 package combinations. +- If the wrapper has an explicit support matrix, reuse one shared test across version-specific scenario entries instead of duplicating the assertions. The AI SDK wrapper scenario uses this for its supported package combinations. ### Runner-wrapper scenario pattern diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index e62bd5b84..f5ba3ad75 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -531,14 +531,6 @@ "label": "AI SDK Instrumentation", "metadataScenario": "ai-sdk-instrumentation", "variants": [ - { - "variantKey": "ai-sdk-v3", - "label": "v3 pinned" - }, - { - "variantKey": "ai-sdk-v3-latest", - "label": "v3 latest" - }, { "variantKey": "ai-sdk-v4", "label": "v4 pinned" diff --git a/e2e/scenarios/ai-sdk-instrumentation/__cassettes__/ai-sdk-v3-latest.cassette.json b/e2e/scenarios/ai-sdk-instrumentation/__cassettes__/ai-sdk-v3-latest.cassette.json deleted file mode 100644 index 69cb213ea..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__cassettes__/ai-sdk-v3-latest.cassette.json +++ /dev/null @@ -1,1148 +0,0 @@ -{ - "entries": [ - { - "callIndex": 0, - "id": "2583f51940dc092e", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:49.537Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 24, - "messages": [ - { - "content": "Reply with the single token PARIS and no punctuation.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "temperature": 0 - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": "PARIS", - "refusal": null, - "role": "assistant" - } - } - ], - "created": 1786504849, - "id": "chatcmpl-EBtoH326GRbZwguUwwJzfA1rP6Ts8", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_fabbcdf5d9", - "usage": { - "completion_tokens": 2, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 18, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 20 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58ab7ea48029-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Wed, 12 Aug 2026 03:20:49 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "274", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999985", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_06b4b55bc7c94ccdb71d80a0aec0fee0" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 1, - "id": "901ff936ff6b7255", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:49.997Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Count from 1 to 3 and include the words one two three.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": true, - "temperature": 0 - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "chunks": [ - "data: {\"id\":\"chatcmpl-EBtoHHiQ2rImeNIBXy6wNKn48cg3c\",\"object\":\"chat.completion.chunk\",\"created\":1786504849,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"QUqa0V\"}", - "data: {\"id\":\"chatcmpl-EBtoHHiQ2rImeNIBXy6wNKn48cg3c\",\"object\":\"chat.completion.chunk\",\"created\":1786504849,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"One\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"WUiC1\"}", - "data: {\"id\":\"chatcmpl-EBtoHHiQ2rImeNIBXy6wNKn48cg3c\",\"object\":\"chat.completion.chunk\",\"created\":1786504849,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"hc4RV6V\"}", - "data: {\"id\":\"chatcmpl-EBtoHHiQ2rImeNIBXy6wNKn48cg3c\",\"object\":\"chat.completion.chunk\",\"created\":1786504849,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" two\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"5bTk\"}", - "data: {\"id\":\"chatcmpl-EBtoHHiQ2rImeNIBXy6wNKn48cg3c\",\"object\":\"chat.completion.chunk\",\"created\":1786504849,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"lX2OKZ9\"}", - "data: {\"id\":\"chatcmpl-EBtoHHiQ2rImeNIBXy6wNKn48cg3c\",\"object\":\"chat.completion.chunk\",\"created\":1786504849,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"Aa\"}", - "data: {\"id\":\"chatcmpl-EBtoHHiQ2rImeNIBXy6wNKn48cg3c\",\"object\":\"chat.completion.chunk\",\"created\":1786504849,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"ApL8mYr\"}", - "data: {\"id\":\"chatcmpl-EBtoHHiQ2rImeNIBXy6wNKn48cg3c\",\"object\":\"chat.completion.chunk\",\"created\":1786504849,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"obfuscation\":\"pn\"}", - "data: [DONE]" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58adb81c8029-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Wed, 12 Aug 2026 03:20:49 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "230", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999982", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_4858eeb509ce41e692f1a591d072d2d9" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 0, - "id": "49a95bf2b3466013", - "matchKey": "POST api.openai.com/v1/embeddings", - "recordedAt": "2026-08-12T03:20:50.133Z", - "request": { - "body": { - "kind": "json", - "value": { - "encoding_format": "float", - "input": ["Paris is the capital of France."], - "model": "text-embedding-3-small" - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/embeddings" - }, - "response": { - "body": { - "kind": "json", - "value": { - "data": [ - { - "embedding": [ - 0.0196075439453125, 0.019073486328125, -0.01314544677734375, - -0.020660400390625, -0.0188140869140625, 0.029632568359375, - -0.0206298828125, 0.0236358642578125, 0.006320953369140625, - -0.01708984375, 0.004039764404296875, 0.0103607177734375, - -0.0289459228515625, 0.0068359375, -0.03192138671875, - 0.0251922607421875, -0.0282745361328125, 0.050262451171875, - 0.0914306640625, -0.02008056640625, 0.0222930908203125, - -0.01297760009765625, -0.0144805908203125, 0.034393310546875, - 0.052734375, -0.0011014938354492188, 0.00817108154296875, - 0.042510986328125, 0.0030460357666015625, 0.05987548828125, - 0.0164642333984375, -0.00787353515625, -0.00244903564453125, - 0.02423095703125, 0.01953125, -0.061065673828125, - -0.013336181640625, -0.03851318359375, 0.0079498291015625, - 0.0276641845703125, 0.0171051025390625, -0.02838134765625, - -0.0286865234375, 0.03277587890625, -0.035919189453125, - -0.047088623046875, -0.00940704345703125, -0.0224761962890625, - 0.02313232421875, -0.0037555694580078125, 0.045654296875, - -0.004848480224609375, 0.02020263671875, 0.00923919677734375, - 0.0044708251953125, 0.02252197265625, 0.03271484375, - 0.0433349609375, 0.0257110595703125, 0.0035400390625, - 0.051788330078125, 0.0206298828125, 0.04046630859375, - 0.01776123046875, -0.00559234619140625, 0.03271484375, - 0.021575927734375, 0.0322265625, 0.050048828125, - 0.02581787109375, 0.0187835693359375, 0.044342041015625, - -0.005947113037109375, 0.006748199462890625, 0.01885986328125, - 0.014678955078125, 0.004638671875, 0.038848876953125, - 0.0173492431640625, -0.03466796875, -0.0016269683837890625, - 0.01055908203125, 0.01372528076171875, -0.02166748046875, - -0.00677490234375, -0.01284027099609375, -0.00388336181640625, - 0.00274658203125, -0.002719879150390625, -0.0220794677734375, - 0.0014619827270507812, -0.01175689697265625, -0.0172119140625, - 0.015655517578125, -0.0308990478515625, 0.00458526611328125, - 0.0211334228515625, -0.04888916015625, -0.058837890625, - 0.02716064453125, 0.027862548828125, 0.001308441162109375, - -0.04681396484375, 0.0316162109375, 0.0155029296875, - -0.01544952392578125, 0.0004296302795410156, - -0.0005507469177246094, 0.01428985595703125, -0.0408935546875, - -0.021759033203125, 0.00412750244140625, -0.00025177001953125, - 0.032440185546875, -0.059783935546875, 0.01093292236328125, - 0.01397705078125, -0.00713348388671875, 0.031005859375, - -0.007572174072265625, -0.006343841552734375, - 0.0026073455810546875, -0.04180908203125, 0.040374755859375, - -0.03387451171875, -0.048248291015625, 0.006519317626953125, - 0.0264739990234375, 0.0227203369140625, -0.00856781005859375, - 0.004657745361328125, -0.044952392578125, 0.016876220703125, - 0.0258941650390625, 0.0506591796875, -0.00958251953125, - 0.033721923828125, -0.0057525634765625, 0.01751708984375, - 0.0177154541015625, -0.01558685302734375, 0.028533935546875, - -0.01058197021484375, 0.036529541015625, -0.0183258056640625, - -0.033843994140625, -0.031402587890625, 0.040069580078125, - 0.01015472412109375, -0.04278564453125, 0.016510009765625, - -0.0212554931640625, 0.00275421142578125, 0.0289459228515625, - -0.0037078857421875, -0.062103271484375, 0.01442718505859375, - -0.0018835067749023438, 0.0384521484375, 0.02569580078125, - -0.036865234375, 0.021331787109375, -0.046173095703125, - -0.016265869140625, -0.0034809112548828125, - 0.0081939697265625, -0.0090179443359375, -0.00542449951171875, - 0.011871337890625, 0.0159454345703125, -0.0235137939453125, - 0.00461578369140625, 0.051116943359375, 0.0389404296875, - 0.0225677490234375, -0.01392364501953125, -0.0185699462890625, - 0.08013916015625, 0.0262603759765625, -0.02142333984375, - -0.01103973388671875, -0.01428985595703125, 0.030731201171875, - -0.01473236083984375, 0.0189361572265625, 0.0250244140625, - -0.0206451416015625, -0.026885986328125, -0.0172882080078125, - 0.05322265625, 0.0615234375, -0.019073486328125, - 0.056396484375, -0.045440673828125, -0.06280517578125, - 0.0241546630859375, 0.0021800994873046875, - 0.006801605224609375, 0.0093231201171875, 0.00798797607421875, - -0.03924560546875, 0.04193115234375, -0.0056304931640625, - -0.00327301025390625, -0.01485443115234375, - 0.0191192626953125, -0.01004791259765625, -0.048309326171875, - 0.0295257568359375, -0.004802703857421875, - -0.0124359130859375, -0.00547027587890625, - -0.0262298583984375, 0.0384521484375, 0.03857421875, - -0.01038360595703125, 0.012908935546875, 0.034027099609375, - 0.0299072265625, -0.00550079345703125, 0.034637451171875, - 0.023406982421875, 0.0113067626953125, -0.01557159423828125, - -0.048919677734375, 0.055419921875, 0.052642822265625, - 0.00609588623046875, -0.044586181640625, 0.002712249755859375, - 0.00432586669921875, -0.028656005859375, -0.0147552490234375, - -0.015411376953125, -0.0269775390625, -0.01922607421875, - 0.003505706787109375, -0.0132598876953125, -0.04779052734375, - 0.019317626953125, -0.0338134765625, 0.01849365234375, - 0.01416015625, 0.0026702880859375, -0.00037384033203125, - 0.0026149749755859375, 0.01383209228515625, 0.037200927734375, - -0.0421142578125, 0.0126953125, 0.043365478515625, - -0.0171966552734375, -0.025360107421875, 0.017303466796875, - -0.03546142578125, 0.035125732421875, 0.006137847900390625, - -0.00630950927734375, -0.047698974609375, -0.0099029541015625, - 0.01043701171875, 0.0198974609375, -0.00995635986328125, - -0.0018243789672851562, -0.00839996337890625, - 0.0192108154296875, -0.0095977783203125, -0.023651123046875, - -0.0224609375, -0.030517578125, 0.03485107421875, - -0.0008287429809570312, -0.0177154541015625, 0.029052734375, - -0.020965576171875, 0.005268096923828125, -0.028167724609375, - 0.0009093284606933594, -0.001140594482421875, - 0.0157928466796875, -0.02178955078125, 0.0443115234375, - -0.01227569580078125, 0.01256561279296875, - -0.0095367431640625, -0.0198211669921875, 0.04046630859375, - 0.06732177734375, -0.032562255859375, 0.052032470703125, - 0.02166748046875, -0.021209716796875, -0.033935546875, - 0.0225982666015625, -0.008056640625, 0.006862640380859375, - 0.035552978515625, 0.0010929107666015625, - 0.006832122802734375, 0.012481689453125, -0.0239105224609375, - -0.0117950439453125, 0.004608154296875, 0.01525115966796875, - -0.049560546875, 0.048431396484375, -0.045501708984375, - 0.0196685791015625, 0.0521240234375, 0.00585174560546875, - -0.054107666015625, -0.036376953125, 0.04632568359375, - -0.06768798828125, -0.01151275634765625, 0.00713348388671875, - -0.0157928466796875, 0.0195770263671875, 0.03448486328125, - 0.01102447509765625, -0.00614166259765625, - 0.01256561279296875, 0.007549285888671875, - 0.0020580291748046875, -0.0272674560546875, 0.031707763671875, - 0.0144805908203125, 0.03839111328125, -0.0278778076171875, - -0.00634765625, 0.0008325576782226562, 0.00878143310546875, - -0.008514404296875, 0.06134033203125, -0.040191650390625, - -0.021484375, 0.003170013427734375, 0.029327392578125, - 0.003055572509765625, -0.00859832763671875, -0.02532958984375, - 0.0270538330078125, -0.035919189453125, 0.0270233154296875, - -0.0033893585205078125, -0.01428985595703125, 0.0377197265625, - 0.001129150390625, -0.005687713623046875, -0.03594970703125, - -0.0191802978515625, -0.0195770263671875, - 0.0004911422729492188, 0.0313720703125, -0.040771484375, - 0.0191802978515625, 0.06414794921875, 0.00946807861328125, - -0.05694580078125, 0.0130462646484375, -0.016510009765625, - -0.052764892578125, -0.0163116455078125, -0.018951416015625, - -0.03668212890625, -0.0084381103515625, - -0.0017042160034179688, -0.00557708740234375, - -0.03851318359375, 0.07061767578125, -0.049896240234375, - -0.0277252197265625, 0.0287322998046875, -0.02880859375, - 0.047088623046875, -0.030364990234375, -0.025054931640625, - -0.008270263671875, -0.04766845703125, -0.02618408203125, - -0.0643310546875, -0.002410888671875, 0.0355224609375, - -0.04705810546875, 0.03204345703125, 0.0369873046875, - 0.043701171875, -0.0550537109375, 0.01226043701171875, - -0.006168365478515625, 0.012237548828125, - 0.004772186279296875, 0.00537872314453125, 0.022552490234375, - -0.03302001953125, -0.0009675025939941406, -0.0103759765625, - -0.00447845458984375, -0.029510498046875, -0.0172576904296875, - 0.0028629302978515625, -0.0390625, 0.01015472412109375, - -0.006866455078125, 0.04296875, 0.028167724609375, - 0.0075225830078125, -0.01715087890625, 0.0215911865234375, - -0.0222625732421875, 0.006572723388671875, -0.037872314453125, - 0.015380859375, -0.003780364990234375, 0.0131683349609375, - 0.0540771484375, 0.0298919677734375, -0.07373046875, - 0.0330810546875, -0.02154541015625, 0.037506103515625, - 0.004802703857421875, -0.058837890625, 0.0113525390625, - 0.0028858184814453125, -0.007354736328125, - -0.01139068603515625, -0.06524658203125, -0.019317626953125, - 0.032623291015625, 0.00508880615234375, 0.021331787109375, - 0.0006260871887207031, -0.008514404296875, - 0.01102447509765625, 0.0150909423828125, 0.0099639892578125, - 0.01019287109375, -0.01428985595703125, 0.00167083740234375, - 0.047515869140625, -0.017364501953125, 0.01197052001953125, - -0.03564453125, -0.0462646484375, -0.017425537109375, - 0.055816650390625, 0.029937744140625, -0.045867919921875, - 0.06005859375, -0.0311737060546875, -0.00281524658203125, - -0.00878143310546875, -0.01318359375, -0.0246124267578125, - 0.0300140380859375, 0.0197296142578125, 0.0699462890625, - 0.00809478759765625, -0.03424072265625, 0.00490570068359375, - 0.046600341796875, -0.00392913818359375, 0.044769287109375, - 0.03271484375, 0.018768310546875, 0.0243072509765625, - 0.019378662109375, -0.038787841796875, -0.03131103515625, - 0.0158538818359375, 0.0335693359375, 0.0224456787109375, - 0.025848388671875, 0.007061004638671875, 0.0049285888671875, - -0.0595703125, 0.038726806640625, -0.0494384765625, - -0.06390380859375, -0.0011005401611328125, 0.0219268798828125, - -0.018798828125, 0.006317138671875, -0.0794677734375, - -0.00662994384765625, 0.00353240966796875, - 0.0020122528076171875, -0.0234527587890625, - 0.0159759521484375, 0.052032470703125, -0.038360595703125, - 0.01372528076171875, -0.00020372867584228516, - 0.021575927734375, -0.00679779052734375, -0.053985595703125, - -0.01056671142578125, -0.0272369384765625, - -0.0236053466796875, 0.023162841796875, 0.00165557861328125, - 0.0499267578125, 0.012908935546875, 0.033233642578125, - 0.08331298828125, 0.01042938232421875, 0.0039825439453125, - 0.00824737548828125, -0.0260772705078125, 0.0299530029296875, - -0.025299072265625, 0.032196044921875, 0.01166534423828125, - -0.010711669921875, -0.00376129150390625, -0.023162841796875, - 0.0157470703125, 0.01163482666015625, 0.002651214599609375, - 0.0198822021484375, 0.0246124267578125, -0.0152740478515625, - 0.004291534423828125, 0.032928466796875, 0.0141143798828125, - -0.024322509765625, -0.007518768310546875, 0.04766845703125, - -0.04901123046875, 0.0227508544921875, -0.0255584716796875, - -0.0141143798828125, -0.01641845703125, 0.0095367431640625, - 0.0183563232421875, 0.0184173583984375, 0.05340576171875, - 0.00008529424667358398, 0.03070068359375, - 0.0038967132568359375, -0.0161285400390625, - -0.006809234619140625, -0.0189361572265625, - -0.01512908935546875, -0.0167999267578125, 0.022003173828125, - -0.027374267578125, -0.03448486328125, -0.016998291015625, - -0.024627685546875, 0.045806884765625, -0.03558349609375, - -0.0439453125, -0.000728607177734375, -0.0036830902099609375, - -0.009368896484375, -0.0039520263671875, 0.026947021484375, - -0.01522064208984375, -0.00968170166015625, 0.06329345703125, - 0.037994384765625, 0.017242431640625, 0.0188751220703125, - 0.036865234375, 0.00426483154296875, 0.01039886474609375, - -0.01483154296875, -0.022216796875, -0.0118408203125, - 0.0484619140625, 0.01296234130859375, 0.0175018310546875, - 0.02081298828125, 0.0182037353515625, 0.010162353515625, - -0.003997802734375, -0.0299224853515625, 0.0654296875, - -0.0054168701171875, 0.01355743408203125, 0.032470703125, - -0.03521728515625, -0.04522705078125, 0.0218353271484375, - 0.0035457611083984375, -0.002376556396484375, -0.0419921875, - -0.0294342041015625, -0.0037860870361328125, - 0.01380157470703125, 0.0174713134765625, -0.0234527587890625, - 0.01428985595703125, 0.0014009475708007812, 0.01141357421875, - 0.0008606910705566406, -0.035858154296875, - -0.01201629638671875, 0.00235748291015625, 0.0251312255859375, - -0.019287109375, -0.0242156982421875, 0.01537322998046875, - -0.036956787109375, 0.00670623779296875, -0.0191650390625, - 0.0223846435546875, -0.007534027099609375, 0.0176849365234375, - -0.0187530517578125, 0.01541900634765625, 0.028289794921875, - -0.005496978759765625, 0.02801513671875, -0.02410888671875, - 0.023956298828125, -0.01284027099609375, -0.00730133056640625, - 0.01430511474609375, 0.03460693359375, -0.031982421875, - 0.0055694580078125, -0.005382537841796875, 0.047088623046875, - 0.0189361572265625, 0.0183868408203125, -0.0099945068359375, - -0.0172576904296875, 0.024444580078125, 0.0141754150390625, - 0.045562744140625, 0.0128021240234375, -0.0233917236328125, - -0.0251007080078125, -0.004085540771484375, - -0.0197906494140625, 0.008392333984375, -0.03021240234375, - 0.041046142578125, 0.0124664306640625, -0.0015897750854492188, - 0.0059967041015625, -0.006702423095703125, 0.0124664306640625, - 0.02105712890625, 0.04742431640625, 0.0295562744140625, - -0.04473876953125, 0.053680419921875, 0.0214385986328125, - -0.01190185546875, 0.003604888916015625, 0.004787445068359375, - 0.03082275390625, -0.00344085693359375, 0.0184173583984375, - -0.0284271240234375, 0.05914306640625, -0.0222625732421875, - -0.0287017822265625, 0.02880859375, -0.035003662109375, - -0.022186279296875, 0.0284881591796875, 0.0223388671875, - 0.0144500732421875, 0.043853759765625, 0.0281829833984375, - -0.047088623046875, -0.03582763671875, 0.04150390625, - 0.003925323486328125, 0.00934600830078125, -0.022003173828125, - -0.004741668701171875, -0.01403045654296875, - -0.04925537109375, -0.0005216598510742188, - -0.0065765380859375, 0.01715087890625, -0.0152130126953125, - 0.0200042724609375, -0.0130615234375, -0.0202484130859375, - -0.0025310516357421875, 0.0059661865234375, - 0.0185699462890625, -0.0101776123046875, -0.021728515625, - -0.01114654541015625, -0.0189361572265625, 0.0223541259765625, - 0.0265960693359375, -0.02960205078125, 0.019287109375, - -0.01416778564453125, 0.0121307373046875, 0.034515380859375, - -0.033782958984375, 0.000728607177734375, -0.03411865234375, - 0.00010061264038085938, 0.0010194778442382812, - 0.0228271484375, 0.009246826171875, -0.0114288330078125, - -0.00408935546875, 0.00986480712890625, -0.016845703125, - -0.039886474609375, -0.00739288330078125, - 0.007213592529296875, 0.0256805419921875, 0.01123809814453125, - 0.009185791015625, -0.03607177734375, 0.0251617431640625, - -0.017852783203125, -0.007503509521484375, -0.037750244140625, - -0.02001953125, -0.01474761962890625, -0.0113372802734375, - 0.0115814208984375, -0.00036406517028808594, - -0.014190673828125, -0.0032482147216796875, - 0.0017681121826171875, -0.0238494873046875, - -0.025299072265625, -0.0113525390625, 0.01398468017578125, - 0.01377105712890625, 0.01200103759765625, 0.00659942626953125, - -0.0007700920104980469, -0.05926513671875, 0.0075531005859375, - -0.033935546875, 0.00881195068359375, 0.002941131591796875, - -0.02313232421875, 0.02215576171875, 0.06939697265625, - -0.0294647216796875, -0.0032405853271484375, - -0.00983428955078125, -0.023223876953125, - -0.00310516357421875, -0.0014495849609375, - 0.01334381103515625, -0.0032253265380859375, 0.033447265625, - -0.0108642578125, 0.020355224609375, 0.0001189112663269043, - -0.0002608299255371094, -0.0404052734375, - 0.0038318634033203125, 0.0113983154296875, - -0.0255584716796875, -0.00572967529296875, 0.0184783935546875, - -0.0272064208984375, 0.01013946533203125, - -0.01021575927734375, -0.0146636962890625, - -0.0161285400390625, -0.022796630859375, 0.026214599609375, - -0.036590576171875, -0.0227813720703125, 0.0120086669921875, - 0.03369140625, -0.04632568359375, -0.012939453125, - -0.03302001953125, -0.035858154296875, 0.0019168853759765625, - -0.006450653076171875, 0.0162506103515625, 0.0193023681640625, - -0.00664520263671875, -0.01361083984375, 0.04168701171875, - -0.047821044921875, 0.011138916015625, -0.0188751220703125, - 0.00011795759201049805, -0.0157470703125, -0.01470947265625, - -0.01255035400390625, 0.0099334716796875, -0.0283355712890625, - 0.053131103515625, -0.02044677734375, -0.027008056640625, - -0.05059814453125, -0.01421356201171875, - 0.0009927749633789062, 0.0079803466796875, 0.0181884765625, - 0.0401611328125, 0.01983642578125, -0.028778076171875, - -0.004673004150390625, 0.0011386871337890625, - 0.0002682209014892578, -0.00746917724609375, - -0.038360595703125, -0.01195526123046875, -0.037689208984375, - 0.039764404296875, -0.0227813720703125, -0.00922393798828125, - -0.038116455078125, 0.0167388916015625, 0.01528167724609375, - -0.0020923614501953125, -0.0123291015625, - -0.007373809814453125, -0.04132080078125, 0.03277587890625, - -0.0265045166015625, 0.033172607421875, 0.0306549072265625, - -0.0022735595703125, 0.0080718994140625, -0.0269317626953125, - 0.0202178955078125, 0.0038299560546875, 0.022674560546875, - 0.00959014892578125, 0.039520263671875, 0.00336456298828125, - -0.0233917236328125, 0.0082855224609375, 0.01971435546875, - -0.035430908203125, 0.05047607421875, 0.006145477294921875, - -0.05767822265625, 0.00943756103515625, -0.005275726318359375, - 0.0142822265625, -0.012359619140625, -0.007305145263671875, - 0.006893157958984375, -0.023101806640625, 0.050323486328125, - -0.0291595458984375, 0.0227203369140625, -0.035614013671875, - 0.0396728515625, 0.00412750244140625, 0.034393310546875, - 0.00354766845703125, -0.032135009765625, -0.0251617431640625, - 0.003032684326171875, -0.0310211181640625, -0.033660888671875, - 0.0011720657348632812, -0.009918212890625, - -0.0187530517578125, -0.038116455078125, -0.0288543701171875, - 0.05987548828125, 0.005603790283203125, -0.032379150390625, - 0.0026187896728515625, 0.01201629638671875, - 0.001361846923828125, 0.0191497802734375, -0.00958251953125, - 0.01006317138671875, -0.01715087890625, -0.0546875, - -0.00728607177734375, 0.0243072509765625, 0.043853759765625, - -0.004398345947265625, -0.0305938720703125, - -0.0135650634765625, -0.0357666015625, -0.01088714599609375, - 0.01580810546875, 0.0311126708984375, 0.01107025146484375, - 0.0009198188781738281, 0.01346588134765625, - -0.00598907470703125, -0.031585693359375, 0.0192718505859375, - -0.0298919677734375, 0.024322509765625, -0.0038299560546875, - -0.03668212890625, -0.03363037109375, 0.01568603515625, - 0.027618408203125, -0.0020351409912109375, 0.029083251953125, - 0.0151824951171875, -0.025604248046875, -0.01169586181640625, - -0.003437042236328125, -0.030426025390625, -0.004547119140625, - -0.027496337890625, -0.017486572265625, 0.016510009765625, - 0.049224853515625, -0.044769287109375, 0.0099945068359375, - 0.03118896484375, -0.033172607421875, 0.018585205078125, - -0.032806396484375, 0.02734375, -0.03875732421875, - -0.05328369140625, 0.0248870849609375, 0.0003790855407714844, - -0.0165863037109375, -0.0008473396301269531, - 0.006267547607421875, -0.0216522216796875, - -0.004703521728515625, 0.0313720703125, -0.004276275634765625, - -0.0088653564453125, -0.01087188720703125, 0.032623291015625, - -0.046142578125, -0.0183563232421875, -0.00930023193359375, - 0.040618896484375, 0.006988525390625, 0.024169921875, - 0.007266998291015625, 0.013397216796875, 0.00238800048828125, - 0.00870513916015625, 0.02020263671875, -0.033050537109375, - 0.054840087890625, 0.023162841796875, -0.0002655982971191406, - -0.050750732421875, -0.023345947265625, -0.01496124267578125, - 0.02587890625, 0.00466156005859375, -0.01174163818359375, - 0.0265960693359375, -0.0006241798400878906, - -0.0022716522216796875, -0.0010480880737304688, - -0.01158905029296875, 0.0088043212890625, 0.01372528076171875, - -0.0122833251953125, 0.0022296905517578125, - -0.0183258056640625, 0.0221405029296875, 0.0283355712890625, - 0.042083740234375, -0.00858306884765625, -0.034881591796875, - 0.0240020751953125, 0.037689208984375, -0.01555633544921875, - -0.039154052734375, 0.05511474609375, 0.01129150390625, - 0.0042266845703125, 0.0019359588623046875, - -0.0081329345703125, 0.01029205322265625, - -0.002567291259765625, -0.0022258758544921875, - -0.0098876953125, -0.013153076171875, -0.035858154296875, - 0.01329803466796875, -0.01081085205078125, 0.0192718505859375, - 0.00901031494140625, 0.00010645389556884766, - 0.0134429931640625, -0.0038509368896484375, 0.01763916015625, - 0.0283355712890625, 0.01259613037109375, 0.00826263427734375, - 0.04632568359375, -0.054901123046875, -0.051239013671875, - 0.030975341796875, 0.0223846435546875, 0.0031890869140625, - -0.01381683349609375, -0.0230712890625, -0.00873565673828125, - 0.036590576171875, -0.010467529296875, -0.03173828125, - -0.004344940185546875, -0.03387451171875, - -0.00930023193359375, 0.0092620849609375, -0.0279541015625, - 0.0113372802734375, 0.0222930908203125, 0.0179901123046875, - -0.006481170654296875, -0.01105499267578125, - -0.005947113037109375, 0.005802154541015625, -0.0125732421875, - -0.00921630859375, -0.0013303756713867188, - -0.0197906494140625, -0.005924224853515625, - -0.0263214111328125, -0.01259613037109375, - -0.005962371826171875, 0.017303466796875, -0.0138397216796875, - -0.042022705078125, 0.0295562744140625, -0.015716552734375, - 0.0291748046875, 0.035552978515625, 0.00788116455078125, - -0.01380157470703125, -0.0011653900146484375, - -0.0090179443359375, -0.001216888427734375, - 0.0025177001953125, 0.0003676414489746094, 0.0197906494140625, - -0.02069091796875, 0.01342010498046875, 0.0030536651611328125, - 0.03173828125, -0.001850128173828125, -0.052215576171875, - -0.023193359375, -0.012176513671875, -0.0166778564453125, - -0.03997802734375, -0.041229248046875, -0.017974853515625, - -0.00385284423828125, 0.005035400390625, - -0.002956390380859375, 0.033843994140625, -0.01116943359375, - 0.0279083251953125, -0.0178070068359375, 0.024627685546875, - 0.032867431640625, 0.003444671630859375, 0.0240478515625, - -0.01445770263671875, 0.040771484375, -0.01230621337890625, - 0.00666046142578125, -0.008087158203125, 0.006805419921875, - -0.004276275634765625, 0.0221405029296875, -0.024749755859375, - 0.0400390625, 0.0138397216796875, 0.0361328125, - -0.0028705596923828125, -0.003787994384765625, - 0.06475830078125, 0.0302886962890625, -0.043304443359375, - -0.04559326171875, 0.020751953125, -0.0275115966796875, - -0.02569580078125, -0.01329803466796875, 0.003986358642578125, - 0.04998779296875, -0.0101776123046875, -0.0172119140625, - 0.048004150390625, 0.014007568359375, -0.0207061767578125, - -0.0168914794921875, 0.0146026611328125, 0.0286712646484375, - 0.03729248046875, -0.0244598388671875, 0.00667572021484375, - -0.0207366943359375, 0.03302001953125, 0.00540924072265625, - -0.0284576416015625, 0.01849365234375, -0.011444091796875, - 0.001476287841796875, -0.005615234375, 0.03778076171875, - 0.02642822265625, 0.01239776611328125, -0.01181793212890625, - 0.03521728515625, 0.013031005859375, 0.01139068603515625, - 0.00672149658203125, 0.024444580078125, 0.0197601318359375, - -0.0177001953125, -0.024200439453125, 0.0216827392578125, - -0.0004317760467529297, 0.00811767578125, -0.0131683349609375, - 0.0012493133544921875, -0.01227569580078125, - 0.0030918121337890625, 0.041107177734375, 0.012481689453125, - -0.029998779296875, -0.0092620849609375, -0.01904296875, - 0.0090484619140625, -0.0301055908203125, -0.00742340087890625, - -0.01364898681640625, 0.0297088623046875, 0.0328369140625, - -0.0116729736328125, 0.00152587890625, 0.045867919921875, - 0.0035400390625, -0.03497314453125, 0.023406982421875, - 0.03289794921875, -0.028076171875, 0.01800537109375, - 0.01024627685546875, -0.004169464111328125, -0.00396728515625, - -0.0245513916015625, -0.01021575927734375, -0.031585693359375, - -0.05474853515625, 0.0168304443359375, 0.026519775390625, - 0.0267333984375, -0.01230621337890625, -0.0149383544921875, - 0.0204620361328125, 0.015380859375, -0.0626220703125, - -0.0162353515625, 0.0220794677734375, -0.0171356201171875, - -0.01561737060546875, -0.035980224609375, 0.0192718505859375, - 0.043701171875, 0.0413818359375, 0.03314208984375, - 0.00016868114471435547, -0.000579833984375, - -0.0177154541015625, -0.037384033203125, 0.0377197265625, - 0.0264739990234375, 0.01340484619140625, 0.02313232421875, - 0.04119873046875, -0.0037288665771484375, -0.0226287841796875, - -0.0005016326904296875, 0.00885772705078125, - -0.0029125213623046875, 0.007633209228515625, 0.00732421875, - -0.0113983154296875, 0.00392913818359375, - -0.00774383544921875, -0.040008544921875, -0.0231781005859375, - 0.007091522216796875, -0.0286865234375, 0.032440185546875, - 0.0177001953125, -0.015167236328125, 0.0082550048828125, - -0.01465606689453125, 0.01073455810546875, - 0.007236480712890625, 0.00146484375, 0.03265380859375, - -0.0084686279296875, 0.0301361083984375, 0.0013427734375, - -0.01102447509765625, -0.0197906494140625, - 0.01364898681640625, -0.031219482421875, -0.0229034423828125, - 0.0275115966796875, 0.0003364086151123047, 0.050811767578125, - 0.0234222412109375, 0.01097869873046875, -0.017974853515625, - -0.0154876708984375, 0.016876220703125, 0.0070037841796875, - 0.00920867919921875, -0.0135040283203125, -0.019073486328125, - 0.028564453125, 0.00928497314453125, 0.030181884765625, - -0.01273345947265625, 0.00492095947265625, - 0.0024242401123046875, -0.06781005859375, -0.00714111328125, - 0.0021343231201171875, 0.003330230712890625, - -0.0007662773132324219, 0.019500732421875, 0.0095062255859375, - -0.0262298583984375, -0.00960540771484375, - 0.006473541259765625, 0.05169677734375, -0.017730712890625, - 0.0238037109375, -0.0214691162109375, 0.0304412841796875, - -0.0126190185546875, -0.03070068359375, -0.037078857421875, - 0.022369384765625, 0.034515380859375, -0.0205841064453125, - 0.004116058349609375, -0.038787841796875, -0.0221710205078125, - -0.024993896484375, -0.021453857421875, - -0.0003190040588378906, 0.0036602020263671875, - 0.0107269287109375, 0.0152435302734375, -0.016265869140625, - -0.0222930908203125, 0.0105133056640625, -0.0185546875, - -0.015045166015625, 0.0134735107421875, 0.002307891845703125, - 0.007579803466796875, -0.0022735595703125, 0.0276336669921875, - 0.0083160400390625, 0.01552581787109375, - -0.007038116455078125, 0.008331298828125, -0.0135955810546875, - 0.020721435546875, 0.050933837890625, 0.0147552490234375, - 0.0205841064453125, -0.00490570068359375, 0.038299560546875, - 0.0301055908203125, 0.05523681640625, 0.00766754150390625, - 0.00782012939453125, -0.00164794921875, 0.0191497802734375, - 0.002674102783203125, -0.017242431640625, 0.02203369140625, - -0.0187835693359375, 0.0350341796875, -0.01007080078125, - -0.04803466796875, 0.01506805419921875, -0.031646728515625, - 0.0182342529296875, 0.0239105224609375, 0.0435791015625, - 0.0156097412109375, -0.0275115966796875, 0.0169525146484375, - -0.0036716461181640625, 0.0154571533203125, - -0.00377655029296875, -0.003070831298828125, - 0.034332275390625, -0.0034275054931640625, - -0.0285797119140625, -0.03607177734375, - -0.0008091926574707031, -0.0127410888671875, -0.036376953125, - -0.007793426513671875, -0.043792724609375, 0.0270538330078125, - -0.058013916015625, -0.01438140869140625, - 0.007251739501953125, 0.045013427734375, 0.00720977783203125, - 0.0254058837890625, 0.054718017578125, 0.0061798095703125, - -0.0198822021484375, 0.010162353515625, -0.035919189453125, - 0.0014743804931640625, -0.0060577392578125, -0.02752685546875, - -0.01453399658203125, -0.0137481689453125, - -0.00218963623046875, -0.0038661956787109375, - 0.006755828857421875, 0.00434112548828125, -0.020294189453125, - 0.0181884765625, -0.004611968994140625, -0.0200042724609375, - 0.034454345703125, -0.0709228515625, -0.028533935546875, - 0.0260772705078125, -0.044677734375, 0.021240234375, - 0.032470703125, 0.0162811279296875, -0.01093292236328125, - -0.01284027099609375, -0.028900146484375, - 0.0009975433349609375, -0.0187835693359375, - 0.0098419189453125, 0.0171966552734375, 0.030670166015625, - 0.00463104248046875, -0.022796630859375, 0.0117950439453125, - -0.028472900390625, -0.021148681640625, -0.039337158203125, - 0.0176544189453125, -0.038238525390625, -0.039581298828125, - 0.002685546875, -0.01483154296875, 0.00533294677734375, - 0.0000928044319152832, 0.01045989990234375, -0.01025390625, - -0.0207061767578125, -0.025665283203125, 0.0222015380859375, - -0.0111083984375, 0.023406982421875, -0.0209197998046875, - -0.0036716461181640625, -0.037689208984375, - 0.0189056396484375, 0.0112457275390625, -0.016204833984375, - 0.0017604827880859375, 0.01241302490234375, - -0.0038776397705078125, -0.00168609619140625, 0.043701171875, - -0.0297698974609375, 0.005054473876953125, - -0.0214385986328125, -0.026611328125, -0.01385498046875, - 0.00682830810546875, -0.0007939338684082031, - 0.030181884765625, 0.051513671875, -0.04296875, - -0.037811279296875, -0.0005049705505371094, - 0.01035308837890625, 0.0136260986328125, 0.025604248046875, - 0.0211334228515625, -0.01605224609375, -0.0228118896484375, - -0.004764556884765625, 0.01377105712890625, - -0.01389312744140625, 0.0256805419921875, - 0.0035343170166015625, -0.026397705078125, - -0.0199737548828125, -0.018646240234375, -0.025054931640625, - 0.02752685546875, 0.01459503173828125, -0.002986907958984375, - 0.038421630859375, -0.0166168212890625, 0.00832366943359375, - 0.06048583984375, 0.0234527587890625, 0.01180267333984375, - 0.00638580322265625, -0.0140838623046875, -0.0074615478515625, - -0.00616455078125, -0.02105712890625, 0.000720977783203125, - -0.01064300537109375, 0.00774383544921875, 0.016632080078125, - 0.0108795166015625, -0.0135498046875, 0.00914764404296875, - 0.034332275390625, -0.01390838623046875, -0.037353515625, - -0.0296173095703125, 0.01849365234375, -0.0230255126953125, - 0.01108551025390625, -0.0214691162109375, 0.0247955322265625, - -0.00872802734375, 0.0184478759765625, 0.00928497314453125, - 0.0202484130859375, 0.0223541259765625, 0.006092071533203125, - 0.035430908203125, -0.035614013671875, -0.0004284381866455078, - -0.0036907196044921875, 0.0053863525390625, - 0.002460479736328125, 0.00722503662109375, - -0.0257110595703125, 0.00926971435546875, 0.00696563720703125, - -0.007740020751953125, 0.01490020751953125, -0.03143310546875, - 0.0160980224609375, -0.0022335052490234375, 0.0146484375, - 0.01129913330078125, 0.019683837890625, -0.0193634033203125, - 0.0092620849609375, 0.01381683349609375, 0.001338958740234375, - 0.01309967041015625, -0.0032100677490234375, - -0.00939178466796875, 0.02569580078125, 0.030731201171875, - 0.023895263671875, 0.0019083023071289062, -0.0124969482421875, - -0.01023101806640625, -0.03558349609375, 0.022857666015625, - -0.000028312206268310547, 0.0123291015625, 0.005157470703125, - -0.01056671142578125, 0.005367279052734375, 0.009490966796875, - 0.005687713623046875, -0.00585174560546875, - 0.01279449462890625, -0.01137542724609375, - 0.005809783935546875, 0.0124359130859375, - -0.006900787353515625, 0.04034423828125, -0.0111083984375, - -0.0012426376342773438, 0.0159454345703125, -0.0450439453125, - -0.01468658447265625, 0.00832366943359375, - -0.01213836669921875, -0.0028018951416015625, - 0.0014476776123046875, -0.00453948974609375, - -0.003498077392578125, -0.0117645263671875, - 0.00293731689453125, -0.0258941650390625, 0.008209228515625, - -0.00203704833984375, 0.0144195556640625, 0.036346435546875, - -0.0119476318359375, -0.03778076171875, 0.0245819091796875, - -0.035858154296875, 0.0084991455078125, 0.01824951171875, - 0.0088653564453125, -0.0467529296875, 0.01412200927734375, - 0.01203155517578125, -0.0232391357421875, - -0.00788116455078125, 0.0123443603515625, -0.031036376953125, - 0.003704071044921875, 0.0136260986328125, - 0.0031147003173828125, 0.0007843971252441406, - 0.00490570068359375 - ], - "index": 0, - "object": "embedding" - } - ], - "model": "text-embedding-3-small", - "object": "list", - "usage": { - "prompt_tokens": 7, - "total_tokens": 7 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58b09a1f8029-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Wed, 12 Aug 2026 03:20:50 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "53", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "10000", - "x-ratelimit-limit-tokens": "10000000", - "x-ratelimit-remaining-requests": "9999", - "x-ratelimit-remaining-tokens": "9999993", - "x-ratelimit-reset-requests": "6ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_19802e0ed763486f94feced58ca31f72" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 2, - "id": "f7a3ad27b5067191", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:50.707Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 128, - "messages": [ - { - "content": "You must inspect live weather via the provided get_weather tool before responding.", - "role": "system" - }, - { - "content": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "temperature": 0, - "tool_choice": "auto", - "tools": [ - { - "function": { - "description": "Get the weather for a location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": ["location"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "tool_calls", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": null, - "refusal": null, - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"location\":\"Paris, France\"}", - "name": "get_weather" - }, - "id": "call_UMuGhN6ZkwsCp9em8q4OFyuV", - "type": "function" - } - ] - } - } - ], - "created": 1786504850, - "id": "chatcmpl-EBtoIGtRYm94JXcy7Q66tHaVUPGNP", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_98fa25fd02", - "usage": { - "completion_tokens": 16, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 87, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 103 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58b15ab98029-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Wed, 12 Aug 2026 03:20:50 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "501", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999952", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_efa3e17526274413b40f8e0ae1515236" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 3, - "id": "26ad5653def30060", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:51.320Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Return JSON with {\"city\":\"Paris\"}.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "temperature": 0, - "tool_choice": { - "function": { - "name": "json" - }, - "type": "function" - }, - "tools": [ - { - "function": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": ["city"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": null, - "refusal": null, - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"city\":\"Paris\"}", - "name": "json" - }, - "id": "call_SQcqBNyCGzH6a0M9YVRrzVRh", - "type": "function" - } - ] - } - } - ], - "created": 1786504850, - "id": "chatcmpl-EBtoI7EcjbAELqo9caSK830h7MXBt", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_9afcdcbeed", - "usage": { - "completion_tokens": 5, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 59, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 64 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58b4ed388029-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Wed, 12 Aug 2026 03:20:51 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "547", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999990", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_0688ec8d78b54eb1adf36824bad5e9a6" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 4, - "id": "1e0515e907a8bb36", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:51.850Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Stream JSON with {\"city\":\"Paris\"}.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": true, - "temperature": 0, - "tool_choice": { - "function": { - "name": "json" - }, - "type": "function" - }, - "tools": [ - { - "function": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": ["city"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "chunks": [ - "data: {\"id\":\"chatcmpl-EBtoJLtlpqaMMJdyJkkyM7JLsRTq7\",\"object\":\"chat.completion.chunk\",\"created\":1786504851,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_WqHLU3gBYZ0G7o9SdYcxHhpm\",\"type\":\"function\",\"function\":{\"name\":\"json\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"kiOb2CCw\"}", - "data: {\"id\":\"chatcmpl-EBtoJLtlpqaMMJdyJkkyM7JLsRTq7\",\"object\":\"chat.completion.chunk\",\"created\":1786504851,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"5XFod0Hptnw\"}", - "data: {\"id\":\"chatcmpl-EBtoJLtlpqaMMJdyJkkyM7JLsRTq7\",\"object\":\"chat.completion.chunk\",\"created\":1786504851,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"city\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"KszwHb7sDw\"}", - "data: {\"id\":\"chatcmpl-EBtoJLtlpqaMMJdyJkkyM7JLsRTq7\",\"object\":\"chat.completion.chunk\",\"created\":1786504851,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"430kM2ZZd\"}", - "data: {\"id\":\"chatcmpl-EBtoJLtlpqaMMJdyJkkyM7JLsRTq7\",\"object\":\"chat.completion.chunk\",\"created\":1786504851,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Paris\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"mbpoFD0Rh\"}", - "data: {\"id\":\"chatcmpl-EBtoJLtlpqaMMJdyJkkyM7JLsRTq7\",\"object\":\"chat.completion.chunk\",\"created\":1786504851,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"N3lxDJJGf8o\"}", - "data: {\"id\":\"chatcmpl-EBtoJLtlpqaMMJdyJkkyM7JLsRTq7\",\"object\":\"chat.completion.chunk\",\"created\":1786504851,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"obfuscation\":\"oc\"}", - "data: [DONE]" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58b8cfd98029-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Wed, 12 Aug 2026 03:20:51 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "396", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999990", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_261be71c049d4ae99c5544c36b60c3a5" - }, - "status": 200, - "statusText": "OK" - } - } - ], - "meta": { - "createdAt": "2026-07-06T14:02:52.301Z" - } -} diff --git a/e2e/scenarios/ai-sdk-instrumentation/__cassettes__/ai-sdk-v3.cassette.json b/e2e/scenarios/ai-sdk-instrumentation/__cassettes__/ai-sdk-v3.cassette.json deleted file mode 100644 index ac6394153..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__cassettes__/ai-sdk-v3.cassette.json +++ /dev/null @@ -1,1151 +0,0 @@ -{ - "entries": [ - { - "callIndex": 0, - "id": "2583f51940dc092e", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:42.139Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 24, - "messages": [ - { - "content": "Reply with the single token PARIS and no punctuation.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "temperature": 0 - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": "PARIS", - "refusal": null, - "role": "assistant" - } - } - ], - "created": 1786504841, - "id": "chatcmpl-EBto9R78okjPTViSZCpqfiBBIKrs5", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_fabbcdf5d9", - "usage": { - "completion_tokens": 2, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 18, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 20 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c587d3abe8029-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Wed, 12 Aug 2026 03:20:42 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "279", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999985", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_84791dd2fdbb49ceb596955e38420fe0" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 1, - "id": "901ff936ff6b7255", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:42.559Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Count from 1 to 3 and include the words one two three.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": true, - "temperature": 0 - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "chunks": [ - "data: {\"id\":\"chatcmpl-EBtoAKna8F7GKBPsPskHdBhRm8Byl\",\"object\":\"chat.completion.chunk\",\"created\":1786504842,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"IbkBYq\"}", - "data: {\"id\":\"chatcmpl-EBtoAKna8F7GKBPsPskHdBhRm8Byl\",\"object\":\"chat.completion.chunk\",\"created\":1786504842,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"One\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"B7cq1\"}", - "data: {\"id\":\"chatcmpl-EBtoAKna8F7GKBPsPskHdBhRm8Byl\",\"object\":\"chat.completion.chunk\",\"created\":1786504842,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"UZIccye\"}", - "data: {\"id\":\"chatcmpl-EBtoAKna8F7GKBPsPskHdBhRm8Byl\",\"object\":\"chat.completion.chunk\",\"created\":1786504842,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" two\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"pMpo\"}", - "data: {\"id\":\"chatcmpl-EBtoAKna8F7GKBPsPskHdBhRm8Byl\",\"object\":\"chat.completion.chunk\",\"created\":1786504842,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"J1nfxcN\"}", - "data: {\"id\":\"chatcmpl-EBtoAKna8F7GKBPsPskHdBhRm8Byl\",\"object\":\"chat.completion.chunk\",\"created\":1786504842,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"IA\"}", - "data: {\"id\":\"chatcmpl-EBtoAKna8F7GKBPsPskHdBhRm8Byl\",\"object\":\"chat.completion.chunk\",\"created\":1786504842,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"17pj5wD\"}", - "data: {\"id\":\"chatcmpl-EBtoAKna8F7GKBPsPskHdBhRm8Byl\",\"object\":\"chat.completion.chunk\",\"created\":1786504842,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"obfuscation\":\"PG\"}", - "data: [DONE]" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c587f7ce48029-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Wed, 12 Aug 2026 03:20:42 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "240", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999985", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_a35d2f97e1e047d9b705abdd4b39a9ae" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 0, - "id": "49a95bf2b3466013", - "matchKey": "POST api.openai.com/v1/embeddings", - "recordedAt": "2026-08-12T03:20:42.754Z", - "request": { - "body": { - "kind": "json", - "value": { - "encoding_format": "float", - "input": ["Paris is the capital of France."], - "model": "text-embedding-3-small" - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/embeddings" - }, - "response": { - "body": { - "kind": "json", - "value": { - "data": [ - { - "embedding": [ - 0.0196380615234375, 0.019073486328125, -0.01320648193359375, - -0.0206451416015625, -0.0188751220703125, 0.02972412109375, - -0.020599365234375, 0.023651123046875, 0.006359100341796875, - -0.017059326171875, 0.004039764404296875, 0.01035308837890625, - -0.028961181640625, 0.00681304931640625, -0.03192138671875, - 0.02520751953125, -0.0282440185546875, 0.0501708984375, - 0.09149169921875, -0.020050048828125, 0.022308349609375, - -0.01296234130859375, -0.01445770263671875, 0.03436279296875, - 0.052764892578125, -0.0010938644409179688, 0.0081634521484375, - 0.042510986328125, 0.003047943115234375, 0.059844970703125, - 0.0164337158203125, -0.0078582763671875, - -0.0024394989013671875, 0.02423095703125, 0.0195159912109375, - -0.061065673828125, -0.0133514404296875, -0.038421630859375, - 0.00791168212890625, 0.027679443359375, 0.01708984375, - -0.0284271240234375, -0.0287322998046875, 0.03277587890625, - -0.035919189453125, -0.04705810546875, -0.0094451904296875, - -0.022491455078125, 0.02313232421875, -0.0037364959716796875, - 0.045654296875, -0.00487518310546875, 0.020172119140625, - 0.009246826171875, 0.00449371337890625, 0.022552490234375, - 0.03277587890625, 0.0433349609375, 0.02569580078125, - 0.0035457611083984375, 0.051788330078125, 0.020599365234375, - 0.04046630859375, 0.0177459716796875, -0.0055999755859375, - 0.03271484375, 0.0215606689453125, 0.032257080078125, - 0.050048828125, 0.0258026123046875, 0.018768310546875, - 0.044342041015625, -0.005954742431640625, 0.00675201416015625, - 0.0188751220703125, 0.01470947265625, 0.00463104248046875, - 0.038787841796875, 0.017364501953125, -0.03466796875, - -0.0016241073608398438, 0.01056671142578125, - 0.0137481689453125, -0.0216827392578125, -0.00678253173828125, - -0.01279449462890625, -0.00383758544921875, - 0.0027370452880859375, -0.0027408599853515625, - -0.0220947265625, 0.0014705657958984375, -0.0117034912109375, - -0.0172882080078125, 0.015655517578125, -0.03094482421875, - 0.00457000732421875, 0.0211029052734375, -0.04888916015625, - -0.058837890625, 0.02716064453125, 0.0279083251953125, - 0.0013456344604492188, -0.046783447265625, 0.0316162109375, - 0.0154876708984375, -0.01546478271484375, - 0.00037980079650878906, -0.0005192756652832031, - 0.0143280029296875, -0.040924072265625, -0.021728515625, - 0.004119873046875, -0.0002334117889404297, 0.032440185546875, - -0.059783935546875, 0.01092529296875, 0.014007568359375, - -0.00711822509765625, 0.031036376953125, - -0.007556915283203125, -0.00634002685546875, 0.0025634765625, - -0.04180908203125, 0.0404052734375, -0.03387451171875, - -0.048248291015625, 0.00656890869140625, 0.0265350341796875, - 0.0227813720703125, -0.00855255126953125, 0.00469970703125, - -0.044952392578125, 0.01690673828125, 0.025909423828125, - 0.05059814453125, -0.0096282958984375, 0.033782958984375, - -0.00574493408203125, 0.0175018310546875, 0.01776123046875, - -0.015625, 0.0285797119140625, -0.01055908203125, - 0.0364990234375, -0.018280029296875, -0.0338134765625, - -0.0313720703125, 0.040069580078125, 0.0101470947265625, - -0.04278564453125, 0.0164794921875, -0.0212554931640625, - 0.00272369384765625, 0.0289459228515625, - -0.003696441650390625, -0.0621337890625, 0.014434814453125, - -0.00196075439453125, 0.0384521484375, 0.025665283203125, - -0.036865234375, 0.0212860107421875, -0.046142578125, - -0.0162811279296875, -0.0034656524658203125, - 0.00821685791015625, -0.00901031494140625, - -0.00539398193359375, 0.011871337890625, 0.0160064697265625, - -0.0234527587890625, 0.004573822021484375, 0.0511474609375, - 0.038909912109375, 0.0225677490234375, -0.0139312744140625, - -0.018524169921875, 0.08013916015625, 0.026275634765625, - -0.0214385986328125, -0.01105499267578125, - -0.01430511474609375, 0.03076171875, -0.01468658447265625, - 0.0189056396484375, 0.0250244140625, -0.0206146240234375, - -0.0269317626953125, -0.0172882080078125, 0.05322265625, - 0.061492919921875, -0.0190887451171875, 0.056365966796875, - -0.04541015625, -0.0628662109375, 0.0241546630859375, - 0.002185821533203125, 0.0067596435546875, 0.00933074951171875, - 0.0079803466796875, -0.039215087890625, 0.041900634765625, - -0.0055999755859375, -0.0033016204833984375, - -0.01486968994140625, 0.019073486328125, -0.0100555419921875, - -0.04833984375, 0.029510498046875, -0.00475311279296875, - -0.01241302490234375, -0.005481719970703125, - -0.0262298583984375, 0.038421630859375, 0.038543701171875, - -0.01038360595703125, 0.012939453125, 0.033966064453125, - 0.0299072265625, -0.005496978759765625, 0.03466796875, - 0.023406982421875, 0.0113525390625, -0.0155792236328125, - -0.048919677734375, 0.055419921875, 0.05267333984375, - 0.00608062744140625, -0.04461669921875, 0.002704620361328125, - 0.00434112548828125, -0.0286407470703125, - -0.01476287841796875, -0.01540374755859375, - -0.0269317626953125, -0.019195556640625, 0.0035400390625, - -0.01326751708984375, -0.047821044921875, 0.0193634033203125, - -0.033843994140625, 0.0185394287109375, 0.0141448974609375, - 0.0026836395263671875, -0.00033974647521972656, - 0.002651214599609375, 0.01383209228515625, 0.03717041015625, - -0.042083740234375, 0.01270294189453125, 0.04339599609375, - -0.0172119140625, -0.025360107421875, 0.0172576904296875, - -0.035430908203125, 0.035125732421875, 0.00611114501953125, - -0.00634002685546875, -0.047698974609375, - -0.00983428955078125, 0.0104217529296875, 0.019866943359375, - -0.00997161865234375, -0.0018606185913085938, - -0.008392333984375, 0.019195556640625, -0.00960540771484375, - -0.023651123046875, -0.022491455078125, -0.030517578125, - 0.034912109375, -0.0008358955383300781, -0.0177154541015625, - 0.0290985107421875, -0.020965576171875, 0.005283355712890625, - -0.028228759765625, 0.0008840560913085938, -0.001129150390625, - 0.015777587890625, -0.0218505859375, 0.0443115234375, - -0.01230621337890625, 0.01255035400390625, - -0.00955963134765625, -0.01983642578125, 0.04046630859375, - 0.0672607421875, -0.0325927734375, 0.052032470703125, - 0.02166748046875, -0.021240234375, -0.033966064453125, - 0.022613525390625, -0.008087158203125, 0.006885528564453125, - 0.03558349609375, 0.0010814666748046875, 0.006816864013671875, - 0.0125274658203125, -0.0239105224609375, -0.01180267333984375, - 0.004608154296875, 0.0152740478515625, -0.04949951171875, - 0.0484619140625, -0.045501708984375, 0.019683837890625, - 0.0521240234375, 0.005802154541015625, -0.054107666015625, - -0.03643798828125, 0.04638671875, -0.0677490234375, - -0.01151275634765625, 0.007213592529296875, - -0.0158538818359375, 0.019561767578125, 0.03448486328125, - 0.011016845703125, -0.0061187744140625, 0.0125732421875, - 0.007537841796875, 0.002048492431640625, -0.0272369384765625, - 0.03173828125, 0.01447296142578125, 0.03839111328125, - -0.02783203125, -0.0063323974609375, 0.0008225440979003906, - 0.00875091552734375, -0.00853729248046875, 0.06134033203125, - -0.0401611328125, -0.021484375, 0.0031414031982421875, - 0.029296875, 0.003040313720703125, -0.0085601806640625, - -0.0253448486328125, 0.0270233154296875, -0.035919189453125, - 0.0270233154296875, -0.0033855438232421875, - -0.01428985595703125, 0.0377197265625, 0.0011196136474609375, - -0.00563812255859375, -0.03594970703125, -0.0191650390625, - -0.0196075439453125, 0.0004887580871582031, 0.0313720703125, - -0.040771484375, 0.0192108154296875, 0.064208984375, - 0.0094451904296875, -0.05694580078125, 0.01303863525390625, - -0.0164947509765625, -0.052764892578125, -0.0163116455078125, - -0.018951416015625, -0.036712646484375, -0.0084228515625, - -0.0016994476318359375, -0.005573272705078125, - -0.03851318359375, 0.07061767578125, -0.049896240234375, - -0.027740478515625, 0.0287628173828125, -0.028839111328125, - 0.04705810546875, -0.0303955078125, -0.0250396728515625, - -0.008270263671875, -0.047637939453125, -0.0262298583984375, - -0.0643310546875, -0.0024242401123046875, 0.0355224609375, - -0.047149658203125, 0.032012939453125, 0.03704833984375, - 0.043701171875, -0.05511474609375, 0.01226806640625, - -0.006153106689453125, 0.012237548828125, 0.00476837158203125, - 0.00540924072265625, 0.02252197265625, -0.03302001953125, - -0.0009093284606933594, -0.0104217529296875, - -0.00444793701171875, -0.0295257568359375, - -0.0172882080078125, 0.002864837646484375, -0.039031982421875, - 0.01010894775390625, -0.006862640380859375, 0.042938232421875, - 0.028167724609375, 0.007511138916015625, -0.0171661376953125, - 0.0216217041015625, -0.022247314453125, 0.00659942626953125, - -0.03790283203125, 0.01537322998046875, - -0.0037822723388671875, 0.01319122314453125, - 0.054046630859375, 0.0298919677734375, -0.07373046875, - 0.033111572265625, -0.021514892578125, 0.03753662109375, - 0.004749298095703125, -0.058837890625, 0.01134490966796875, - 0.0028514862060546875, -0.007328033447265625, - -0.01137542724609375, -0.06524658203125, -0.0193023681640625, - 0.03265380859375, 0.00508880615234375, 0.0213623046875, - 0.0005936622619628906, -0.0084991455078125, - 0.0110015869140625, 0.01507568359375, 0.0099639892578125, - 0.01019287109375, -0.01430511474609375, 0.0016298294067382812, - 0.047576904296875, -0.017364501953125, 0.01197052001953125, - -0.03570556640625, -0.0462646484375, -0.0174102783203125, - 0.055755615234375, 0.0299072265625, -0.045867919921875, - 0.060028076171875, -0.031219482421875, -0.002826690673828125, - -0.0088043212890625, -0.01319122314453125, - -0.0246429443359375, 0.0300750732421875, 0.0197296142578125, - 0.06988525390625, 0.00811767578125, -0.03424072265625, - 0.004909515380859375, 0.046600341796875, - -0.0038928985595703125, 0.044769287109375, 0.032745361328125, - 0.018707275390625, 0.024261474609375, 0.0193939208984375, - -0.038818359375, -0.031280517578125, 0.015869140625, - 0.0335693359375, 0.0224456787109375, 0.02581787109375, - 0.007049560546875, 0.00487518310546875, -0.059600830078125, - 0.03875732421875, -0.0494384765625, -0.06390380859375, - -0.0011310577392578125, 0.021942138671875, - -0.0187530517578125, 0.0063018798828125, -0.0794677734375, - -0.00656890869140625, 0.00356292724609375, - 0.0019741058349609375, -0.0234832763671875, - 0.0159759521484375, 0.052032470703125, -0.038421630859375, - 0.01377105712890625, -0.00019228458404541016, - 0.0215606689453125, -0.006824493408203125, -0.053955078125, - -0.0105743408203125, -0.0272216796875, -0.02362060546875, - 0.023193359375, 0.0016756057739257812, 0.0499267578125, - 0.0129547119140625, 0.033172607421875, 0.08331298828125, - 0.0103759765625, 0.003932952880859375, 0.00826263427734375, - -0.0260772705078125, 0.0300445556640625, -0.0252685546875, - 0.0322265625, 0.01165008544921875, -0.01071929931640625, - -0.0037631988525390625, -0.02313232421875, 0.0157470703125, - 0.0116424560546875, 0.0026683807373046875, 0.0199127197265625, - 0.0246124267578125, -0.01528167724609375, 0.004302978515625, - 0.032928466796875, 0.01409149169921875, -0.024322509765625, - -0.007480621337890625, 0.04766845703125, -0.04901123046875, - 0.0227203369140625, -0.0255889892578125, -0.014129638671875, - -0.0164337158203125, 0.0095062255859375, 0.01837158203125, - 0.01837158203125, 0.05340576171875, 0.00007402896881103516, - 0.03070068359375, 0.003925323486328125, -0.0161285400390625, - -0.00675201416015625, -0.0189666748046875, - -0.01511383056640625, -0.0167999267578125, 0.0219573974609375, - -0.0273590087890625, -0.034515380859375, -0.01702880859375, - -0.024627685546875, 0.045806884765625, -0.03558349609375, - -0.0439453125, -0.0007295608520507812, -0.0037212371826171875, - -0.00934600830078125, -0.00396728515625, 0.0269317626953125, - -0.01520538330078125, -0.00968170166015625, 0.063232421875, - 0.0379638671875, 0.0172882080078125, 0.01885986328125, - 0.036865234375, 0.00424957275390625, 0.010406494140625, - -0.01483917236328125, -0.022186279296875, - -0.01180267333984375, 0.0484619140625, 0.01297760009765625, - 0.0174713134765625, 0.020751953125, 0.01824951171875, - 0.01013946533203125, -0.00399017333984375, -0.029998779296875, - 0.0654296875, -0.005413055419921875, 0.0135650634765625, - 0.032470703125, -0.03521728515625, -0.04522705078125, - 0.0218505859375, 0.003498077392578125, -0.002376556396484375, - -0.0419921875, -0.0294647216796875, -0.003753662109375, - 0.01380157470703125, 0.0174560546875, -0.0234375, - 0.01435089111328125, 0.0014057159423828125, - 0.0113372802734375, 0.000843048095703125, -0.035858154296875, - -0.01200103759765625, 0.0024013519287109375, - 0.0251007080078125, -0.0193023681640625, -0.0242462158203125, - 0.01535797119140625, -0.0369873046875, 0.00672149658203125, - -0.0191650390625, 0.022369384765625, -0.0075225830078125, - 0.0176544189453125, -0.0187530517578125, 0.0154266357421875, - 0.028350830078125, -0.00547027587890625, 0.0279541015625, - -0.0240631103515625, 0.02398681640625, -0.0128021240234375, - -0.007293701171875, 0.0142974853515625, 0.03460693359375, - -0.032012939453125, 0.005584716796875, -0.00534820556640625, - 0.04705810546875, 0.018890380859375, 0.0184326171875, - -0.01000213623046875, -0.0172882080078125, 0.0244140625, - 0.01416778564453125, 0.045562744140625, 0.012786865234375, - -0.0233917236328125, -0.025146484375, -0.00409698486328125, - -0.019775390625, 0.00835418701171875, -0.0301971435546875, - 0.041046142578125, 0.012481689453125, -0.0015058517456054688, - 0.0059967041015625, -0.00667572021484375, 0.01248931884765625, - 0.02105712890625, 0.04742431640625, 0.0295562744140625, - -0.04473876953125, 0.05364990234375, 0.021392822265625, - -0.01189422607421875, 0.003597259521484375, - 0.004817962646484375, 0.03076171875, -0.0034389495849609375, - 0.0184326171875, -0.0283966064453125, 0.05914306640625, - -0.0222625732421875, -0.0287017822265625, 0.028778076171875, - -0.0350341796875, -0.0222015380859375, 0.02850341796875, - 0.022308349609375, 0.01444244384765625, 0.04388427734375, - 0.028167724609375, -0.04718017578125, -0.03582763671875, - 0.04150390625, 0.003902435302734375, 0.0093536376953125, - -0.02203369140625, -0.004726409912109375, -0.0140228271484375, - -0.049285888671875, -0.0005688667297363281, -0.006591796875, - 0.0171356201171875, -0.01517486572265625, 0.0199432373046875, - -0.013031005859375, -0.0202789306640625, - -0.002544403076171875, 0.005962371826171875, 0.0185546875, - -0.01016998291015625, -0.021697998046875, - -0.01117706298828125, -0.0189361572265625, 0.0223541259765625, - 0.0265960693359375, -0.0296173095703125, 0.019287109375, - -0.0141754150390625, 0.01216888427734375, 0.034698486328125, - -0.033782958984375, 0.0007343292236328125, -0.034149169921875, - 0.0001068115234375, 0.0009908676147460938, 0.0228271484375, - 0.00920867919921875, -0.01145172119140625, - -0.004093170166015625, 0.00984954833984375, - -0.0168609619140625, -0.039886474609375, - -0.007373809814453125, 0.007198333740234375, - 0.025665283203125, 0.01123046875, 0.0092315673828125, - -0.03607177734375, 0.0251922607421875, -0.01788330078125, - -0.007472991943359375, -0.03778076171875, -0.0200042724609375, - -0.01473236083984375, -0.01132965087890625, 0.0115966796875, - -0.0003600120544433594, -0.014190673828125, - -0.0032367706298828125, 0.0017652511596679688, - -0.023895263671875, -0.02532958984375, -0.0113525390625, - 0.0139923095703125, 0.01377105712890625, 0.01204681396484375, - 0.00659942626953125, -0.0007576942443847656, - -0.05926513671875, 0.007549285888671875, -0.033905029296875, - 0.00885009765625, 0.00292205810546875, -0.023193359375, - 0.022186279296875, 0.06939697265625, -0.02947998046875, - -0.0032253265380859375, -0.009796142578125, - -0.0232391357421875, -0.0031147003173828125, - -0.0014448165893554688, 0.0133056640625, - -0.003246307373046875, 0.033447265625, -0.0108184814453125, - 0.02032470703125, 0.00016582012176513672, - -0.0002765655517578125, -0.0404052734375, 0.00383758544921875, - 0.01140594482421875, -0.025543212890625, - -0.005756378173828125, 0.0184478759765625, -0.027191162109375, - 0.010162353515625, -0.01020050048828125, -0.0146636962890625, - -0.01611328125, -0.0227813720703125, 0.0262298583984375, - -0.036590576171875, -0.0228118896484375, 0.01200103759765625, - 0.03369140625, -0.04632568359375, -0.012908935546875, - -0.033050537109375, -0.035858154296875, 0.001895904541015625, - -0.00644683837890625, 0.0162811279296875, 0.0193023681640625, - -0.00662994384765625, -0.01361846923828125, 0.04168701171875, - -0.0478515625, 0.01113128662109375, -0.018890380859375, - 0.00014603137969970703, -0.015716552734375, - -0.01471710205078125, -0.01258087158203125, 0.00994873046875, - -0.028350830078125, 0.05316162109375, -0.02044677734375, - -0.0269927978515625, -0.0506591796875, -0.0142364501953125, - 0.0009512901306152344, 0.00801849365234375, - 0.0181732177734375, 0.04010009765625, 0.01983642578125, - -0.028778076171875, -0.004695892333984375, - 0.0011377334594726562, 0.0002841949462890625, - -0.007450103759765625, -0.038360595703125, - -0.01195526123046875, -0.037689208984375, 0.03973388671875, - -0.0227813720703125, -0.0092620849609375, -0.038116455078125, - 0.0167388916015625, 0.01529693603515625, - -0.002117156982421875, -0.01233673095703125, - -0.00740814208984375, -0.04132080078125, 0.03277587890625, - -0.026519775390625, 0.033172607421875, 0.0306549072265625, - -0.0022640228271484375, 0.0081024169921875, - -0.0269317626953125, 0.020233154296875, 0.0038509368896484375, - 0.0226898193359375, 0.00959014892578125, 0.039459228515625, - 0.003368377685546875, -0.0233306884765625, 0.0083160400390625, - 0.01971435546875, -0.035430908203125, 0.0504150390625, - 0.006153106689453125, -0.05767822265625, 0.00940704345703125, - -0.005260467529296875, 0.01422882080078125, - -0.0123748779296875, -0.00731658935546875, - 0.006916046142578125, -0.0230865478515625, 0.05035400390625, - -0.0291595458984375, 0.022705078125, -0.03564453125, - 0.039642333984375, 0.004154205322265625, 0.03436279296875, - 0.0035457611083984375, -0.0321044921875, -0.025146484375, - 0.003063201904296875, -0.030975341796875, -0.033599853515625, - 0.0011835098266601562, -0.00994873046875, -0.018798828125, - -0.03802490234375, -0.0288848876953125, 0.05987548828125, - 0.005603790283203125, -0.032440185546875, - 0.0026149749755859375, 0.011993408203125, - 0.001346588134765625, 0.0190887451171875, -0.009613037109375, - 0.01007080078125, -0.0171356201171875, -0.0546875, - -0.007312774658203125, 0.0242767333984375, 0.04388427734375, - -0.00441741943359375, -0.030609130859375, -0.0135498046875, - -0.0357666015625, -0.0108795166015625, 0.0157928466796875, - 0.0311126708984375, 0.01108551025390625, - 0.0009121894836425781, 0.01349639892578125, - -0.005985260009765625, -0.031585693359375, 0.019256591796875, - -0.029876708984375, 0.02435302734375, -0.0038127899169921875, - -0.036712646484375, -0.033660888671875, 0.0157012939453125, - 0.027618408203125, -0.0020122528076171875, 0.0290679931640625, - 0.01519775390625, -0.025604248046875, -0.01172637939453125, - -0.003452301025390625, -0.0303955078125, -0.00453948974609375, - -0.027496337890625, -0.01751708984375, 0.0164794921875, - 0.049224853515625, -0.044769287109375, 0.0099639892578125, - 0.03118896484375, -0.033203125, 0.0185546875, - -0.03277587890625, 0.02734375, -0.038726806640625, - -0.053253173828125, 0.024932861328125, 0.00039267539978027344, - -0.016571044921875, -0.0008702278137207031, - 0.006275177001953125, -0.021728515625, -0.004669189453125, - 0.031402587890625, -0.0042877197265625, -0.0088653564453125, - -0.010894775390625, 0.032623291015625, -0.046173095703125, - -0.0183563232421875, -0.0092620849609375, 0.040618896484375, - 0.006977081298828125, 0.0242156982421875, - 0.007274627685546875, 0.0134124755859375, 0.00235748291015625, - 0.00870513916015625, 0.0201873779296875, -0.033050537109375, - 0.05487060546875, 0.0231170654296875, -0.0002980232238769531, - -0.050750732421875, -0.0233917236328125, -0.01499176025390625, - 0.02587890625, 0.00461578369140625, -0.01174163818359375, - 0.026611328125, -0.0006389617919921875, -0.002239227294921875, - -0.0010595321655273438, -0.01160430908203125, - 0.008819580078125, 0.01372528076171875, -0.01226806640625, - 0.002239227294921875, -0.0183258056640625, 0.0221405029296875, - 0.0283203125, 0.0421142578125, -0.00855255126953125, - -0.034881591796875, 0.0240020751953125, 0.0377197265625, - -0.0155029296875, -0.03912353515625, 0.055145263671875, - 0.011260986328125, 0.004222869873046875, - 0.0019702911376953125, -0.0081024169921875, - 0.0102691650390625, -0.0025806427001953125, - -0.0022258758544921875, -0.00988006591796875, - -0.013153076171875, -0.035858154296875, 0.0132598876953125, - -0.0107879638671875, 0.019256591796875, 0.009033203125, - 0.00010395050048828125, 0.01342010498046875, -0.00390625, - 0.0177001953125, 0.028350830078125, 0.0125732421875, - 0.00830841064453125, 0.04632568359375, -0.054901123046875, - -0.051239013671875, 0.031005859375, 0.02239990234375, - 0.0031948089599609375, -0.01383209228515625, -0.0230712890625, - -0.00870513916015625, 0.03656005859375, -0.01050567626953125, - -0.031768798828125, -0.00438690185546875, -0.033843994140625, - -0.00930023193359375, 0.00926971435546875, -0.0279541015625, - 0.01134490966796875, 0.02227783203125, 0.017974853515625, - -0.006488800048828125, -0.0110931396484375, - -0.005939483642578125, 0.005809783935546875, -0.0125732421875, - -0.0092620849609375, -0.0013303756713867188, - -0.0197906494140625, -0.005931854248046875, -0.02630615234375, - -0.01263427734375, -0.006008148193359375, 0.017303466796875, - -0.0138702392578125, -0.0419921875, 0.0295562744140625, - -0.015716552734375, 0.0291748046875, 0.0355224609375, - 0.00782012939453125, -0.013824462890625, - -0.0011453628540039062, -0.009033203125, - -0.0012044906616210938, 0.002529144287109375, - 0.0003771781921386719, 0.01983642578125, -0.0207061767578125, - 0.01340484619140625, 0.003055572509765625, 0.031707763671875, - -0.0019130706787109375, -0.05224609375, -0.023223876953125, - -0.012176513671875, -0.01666259765625, -0.040008544921875, - -0.041229248046875, -0.01800537109375, -0.0038776397705078125, - 0.005023956298828125, -0.0029544830322265625, - 0.03387451171875, -0.011199951171875, 0.0279388427734375, - -0.01776123046875, 0.0246124267578125, 0.032867431640625, - 0.0034275054931640625, 0.0240325927734375, -0.014434814453125, - 0.040771484375, -0.0123138427734375, 0.006633758544921875, - -0.00806427001953125, 0.006805419921875, -0.00426483154296875, - 0.0221405029296875, -0.024749755859375, 0.040069580078125, - 0.013824462890625, 0.0361328125, -0.0028667449951171875, - -0.0037746429443359375, 0.06475830078125, 0.030303955078125, - -0.043304443359375, -0.04559326171875, 0.0207366943359375, - -0.0275115966796875, -0.0256805419921875, -0.01324462890625, - 0.004016876220703125, 0.050048828125, -0.01021575927734375, - -0.0172119140625, 0.048004150390625, 0.0139617919921875, - -0.0207061767578125, -0.0168914794921875, 0.0146026611328125, - 0.0286407470703125, 0.037322998046875, -0.0244598388671875, - 0.006633758544921875, -0.0207366943359375, 0.03302001953125, - 0.005397796630859375, -0.0284423828125, 0.0184783935546875, - -0.01146697998046875, 0.0014848709106445312, - -0.005641937255859375, 0.03778076171875, 0.0264434814453125, - 0.0123748779296875, -0.01178741455078125, 0.03521728515625, - 0.013092041015625, 0.01140594482421875, 0.00672149658203125, - 0.024505615234375, 0.019775390625, -0.0177154541015625, - -0.0242156982421875, 0.02166748046875, -0.0004470348358154297, - 0.00811004638671875, -0.013153076171875, - 0.0012683868408203125, -0.01226806640625, - 0.003078460693359375, 0.041107177734375, 0.0124969482421875, - -0.030029296875, -0.0092620849609375, -0.01904296875, - 0.00907135009765625, -0.0301055908203125, - -0.007450103759765625, -0.01367950439453125, - 0.0297698974609375, 0.032867431640625, -0.0117034912109375, - 0.0015239715576171875, 0.045867919921875, 0.00350189208984375, - -0.034942626953125, 0.023406982421875, 0.03289794921875, - -0.0280914306640625, 0.018035888671875, 0.010223388671875, - -0.00412750244140625, -0.003978729248046875, - -0.024566650390625, -0.01024627685546875, -0.031585693359375, - -0.05474853515625, 0.016815185546875, 0.0265045166015625, - 0.02679443359375, -0.01230621337890625, -0.014892578125, - 0.02044677734375, 0.0153656005859375, -0.06256103515625, - -0.0162506103515625, 0.0221099853515625, -0.017120361328125, - -0.015594482421875, -0.035980224609375, 0.0192718505859375, - 0.043701171875, 0.0413818359375, 0.03314208984375, - 0.0001302957534790039, -0.00060272216796875, - -0.0177154541015625, -0.037384033203125, 0.037750244140625, - 0.0265045166015625, 0.01338958740234375, 0.0231170654296875, - 0.04119873046875, -0.0037441253662109375, -0.0226898193359375, - -0.000499725341796875, 0.00890350341796875, - -0.002899169921875, 0.007648468017578125, - 0.007366180419921875, -0.0113983154296875, - 0.003940582275390625, -0.007781982421875, -0.0400390625, - -0.0231781005859375, 0.007110595703125, -0.0287017822265625, - 0.032440185546875, 0.0176849365234375, -0.01515960693359375, - 0.00826263427734375, -0.0146636962890625, 0.01073455810546875, - 0.007213592529296875, 0.0014743804931640625, 0.03265380859375, - -0.00846099853515625, 0.03009033203125, 0.0013189315795898438, - -0.01103973388671875, -0.01983642578125, 0.0136566162109375, - -0.0312347412109375, -0.0228729248046875, 0.0274658203125, - 0.00033664703369140625, 0.050811767578125, 0.0234222412109375, - 0.011016845703125, -0.01800537109375, -0.0154876708984375, - 0.016876220703125, 0.006988525390625, 0.00920867919921875, - -0.01349639892578125, -0.0190887451171875, 0.028564453125, - 0.0092926025390625, 0.03021240234375, -0.01274871826171875, - 0.0049285888671875, 0.0024471282958984375, -0.06781005859375, - -0.007171630859375, 0.002124786376953125, 0.00334930419921875, - -0.0007500648498535156, 0.01947021484375, 0.0095367431640625, - -0.0262298583984375, -0.00958251953125, 0.00647735595703125, - 0.051727294921875, -0.0177154541015625, 0.0238189697265625, - -0.021453857421875, 0.030426025390625, -0.01262664794921875, - -0.0307159423828125, -0.037078857421875, 0.0223541259765625, - 0.03448486328125, -0.020599365234375, 0.0041046142578125, - -0.038787841796875, -0.0221710205078125, -0.0249786376953125, - -0.0214385986328125, -0.0003192424774169922, - 0.0036640167236328125, 0.0107574462890625, 0.0152435302734375, - -0.0162811279296875, -0.0222930908203125, 0.01052093505859375, - -0.0185699462890625, -0.01507568359375, 0.013458251953125, - 0.0023326873779296875, 0.00759124755859375, - -0.0022640228271484375, 0.0276336669921875, - 0.0083465576171875, 0.0155181884765625, -0.00701904296875, - 0.00833892822265625, -0.01355743408203125, 0.02069091796875, - 0.050933837890625, 0.01476287841796875, 0.0205841064453125, - -0.004924774169921875, 0.038299560546875, 0.0301361083984375, - 0.055267333984375, 0.00765228271484375, 0.00782012939453125, - -0.0016546249389648438, 0.0191497802734375, - 0.0026912689208984375, -0.0172119140625, 0.0219879150390625, - -0.0187835693359375, 0.035064697265625, -0.010101318359375, - -0.048065185546875, 0.0150604248046875, -0.031707763671875, - 0.0182647705078125, 0.02392578125, 0.043548583984375, - 0.01558685302734375, -0.0275115966796875, 0.01690673828125, - -0.003688812255859375, 0.01544952392578125, - -0.0037899017333984375, -0.0030841827392578125, - 0.0343017578125, -0.003437042236328125, -0.0285797119140625, - -0.03607177734375, -0.0008149147033691406, -0.01275634765625, - -0.036376953125, -0.007778167724609375, -0.04376220703125, - 0.027099609375, -0.05804443359375, -0.0144195556640625, - 0.0072479248046875, 0.045013427734375, 0.007221221923828125, - 0.025390625, 0.054718017578125, 0.00618743896484375, - -0.0198974609375, 0.0101470947265625, -0.035919189453125, - 0.00145721435546875, -0.00609588623046875, -0.027557373046875, - -0.01454925537109375, -0.0137176513671875, -0.002197265625, - -0.0038280487060546875, 0.006763458251953125, - 0.00432586669921875, -0.0203094482421875, 0.0181732177734375, - -0.00463104248046875, -0.0200042724609375, 0.03448486328125, - -0.0709228515625, -0.028533935546875, 0.0260162353515625, - -0.044677734375, 0.0212249755859375, 0.032501220703125, - 0.0163116455078125, -0.01091766357421875, - -0.01284027099609375, -0.028900146484375, - 0.0009822845458984375, -0.0187835693359375, - 0.0098114013671875, 0.0172119140625, 0.0307159423828125, - 0.004657745361328125, -0.0228271484375, 0.01180267333984375, - -0.028472900390625, -0.0211639404296875, -0.03936767578125, - 0.0176239013671875, -0.038238525390625, -0.03961181640625, - 0.0026645660400390625, -0.01483154296875, - 0.005336761474609375, 0.00011247396469116211, - 0.01042938232421875, -0.01023101806640625, -0.020721435546875, - -0.0256500244140625, 0.022186279296875, -0.01110076904296875, - 0.0234222412109375, -0.0209197998046875, - -0.0036983489990234375, -0.037628173828125, - 0.0189056396484375, 0.0112762451171875, -0.0162200927734375, - 0.00176239013671875, 0.01241302490234375, - -0.0038700103759765625, -0.0017251968383789062, - 0.043731689453125, -0.0298004150390625, 0.0050811767578125, - -0.0214385986328125, -0.0266265869140625, - -0.01386260986328125, 0.006877899169921875, - -0.0007877349853515625, 0.03021240234375, 0.051544189453125, - -0.04302978515625, -0.037811279296875, -0.0004911422729492188, - 0.0103607177734375, 0.0136260986328125, 0.025604248046875, - 0.0211639404296875, -0.0160675048828125, -0.022796630859375, - -0.004734039306640625, 0.013763427734375, - -0.01389312744140625, 0.0256195068359375, - 0.0035343170166015625, -0.026397705078125, - -0.0199432373046875, -0.018646240234375, -0.0250701904296875, - 0.0275421142578125, 0.0146026611328125, -0.002964019775390625, - 0.03839111328125, -0.0166168212890625, 0.00832366943359375, - 0.060455322265625, 0.0234527587890625, 0.0117950439453125, - 0.006397247314453125, -0.014068603515625, - -0.007488250732421875, -0.00616455078125, -0.0210723876953125, - 0.0007615089416503906, -0.01064300537109375, 0.00775146484375, - 0.016632080078125, 0.01084136962890625, -0.01355743408203125, - 0.0091552734375, 0.03436279296875, -0.013916015625, - -0.037353515625, -0.029632568359375, 0.018524169921875, - -0.0230712890625, 0.01107025146484375, -0.021484375, - 0.0247955322265625, -0.00873565673828125, 0.0184783935546875, - 0.00930023193359375, 0.0202484130859375, 0.0223541259765625, - 0.00608062744140625, 0.035430908203125, -0.035614013671875, - -0.0004277229309082031, -0.003662109375, 0.00539398193359375, - 0.0024127960205078125, 0.007221221923828125, - -0.0256805419921875, 0.00926971435546875, 0.00698089599609375, - -0.0077667236328125, 0.014862060546875, -0.031402587890625, - 0.0160675048828125, -0.0022525787353515625, - 0.01468658447265625, 0.01129913330078125, 0.019622802734375, - -0.0193939208984375, 0.00928497314453125, 0.013824462890625, - 0.0013227462768554688, 0.0131072998046875, - -0.0032062530517578125, -0.0093841552734375, - 0.0257415771484375, 0.0307464599609375, 0.02386474609375, - 0.0018949508666992188, -0.01251220703125, -0.010223388671875, - -0.035614013671875, 0.0228118896484375, - -0.000028192996978759766, 0.0123138427734375, 0.005126953125, - -0.010589599609375, 0.00533294677734375, 0.009490966796875, - 0.00566864013671875, -0.005893707275390625, - 0.01282501220703125, -0.0113372802734375, 0.00579071044921875, - 0.01239776611328125, -0.0068359375, 0.04034423828125, - -0.011077880859375, -0.0012264251708984375, 0.015899658203125, - -0.0450439453125, -0.01468658447265625, 0.0083160400390625, - -0.0121612548828125, -0.0027618408203125, - 0.0014629364013671875, -0.004535675048828125, - -0.0034885406494140625, -0.01180267333984375, 0.0029296875, - -0.025909423828125, 0.00821685791015625, - -0.0020427703857421875, 0.014404296875, 0.036346435546875, - -0.01197052001953125, -0.03778076171875, 0.0245819091796875, - -0.035858154296875, 0.00846099853515625, 0.0182342529296875, - 0.0088653564453125, -0.0467529296875, 0.01409912109375, - 0.01203155517578125, -0.0232696533203125, -0.0079498291015625, - 0.0123291015625, -0.0310516357421875, 0.00372314453125, - 0.0136260986328125, 0.00312042236328125, - 0.0007691383361816406, 0.004894256591796875 - ], - "index": 0, - "object": "embedding" - } - ], - "model": "text-embedding-3-small", - "object": "list", - "usage": { - "prompt_tokens": 7, - "total_tokens": 7 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58821f188029-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Wed, 12 Aug 2026 03:20:42 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "95", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "10000", - "x-ratelimit-limit-tokens": "10000000", - "x-ratelimit-remaining-requests": "9999", - "x-ratelimit-remaining-tokens": "9999993", - "x-ratelimit-reset-requests": "6ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_1b8ae0d06d8d4e85b5adf955e9278634" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 2, - "id": "f7a3ad27b5067191", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:43.286Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 128, - "messages": [ - { - "content": "You must inspect live weather via the provided get_weather tool before responding.", - "role": "system" - }, - { - "content": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "temperature": 0, - "tool_choice": "auto", - "tools": [ - { - "function": { - "description": "Get the weather for a location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": ["location"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "tool_calls", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": null, - "refusal": null, - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"location\":\"Paris, France\"}", - "name": "get_weather" - }, - "id": "call_7m4SqfMove1q1WJKoZ9L8vBF", - "type": "function" - } - ] - } - } - ], - "created": 1786504842, - "id": "chatcmpl-EBtoAmVcdUeW509E4AjMQHaUMKh64", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_98fa25fd02", - "usage": { - "completion_tokens": 16, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 87, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 103 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58833ff58029-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Wed, 12 Aug 2026 03:20:43 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "461", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999952", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_441d4ebf09e94dbc93999d7b512958f2" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 3, - "id": "26ad5653def30060", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:43.735Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Return JSON with {\"city\":\"Paris\"}.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "temperature": 0, - "tool_choice": { - "function": { - "name": "json" - }, - "type": "function" - }, - "tools": [ - { - "function": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": ["city"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": null, - "refusal": null, - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"city\":\"Paris\"}", - "name": "json" - }, - "id": "call_NaJO7RcNWWu9cXZ0MghmDigY", - "type": "function" - } - ] - } - } - ], - "created": 1786504843, - "id": "chatcmpl-EBtoBrlB2T86JYR73gtUJwn7DAbYf", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_9afcdcbeed", - "usage": { - "completion_tokens": 5, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 59, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 64 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58868ac18029-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Wed, 12 Aug 2026 03:20:43 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "380", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999990", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_7ae5bcb488284eca9142bcc527b04043" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 4, - "id": "1e0515e907a8bb36", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-12T03:20:44.319Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Stream JSON with {\"city\":\"Paris\"}.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": true, - "temperature": 0, - "tool_choice": { - "function": { - "name": "json" - }, - "type": "function" - }, - "tools": [ - { - "function": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": ["city"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "chunks": [ - "data: {\"id\":\"chatcmpl-EBtoCBiLNVza52zxcVcNSx4yVdz3P\",\"object\":\"chat.completion.chunk\",\"created\":1786504844,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_jdXI9wWsrTj9Hoenr1l6mYu9\",\"type\":\"function\",\"function\":{\"name\":\"json\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"OQrXmj1E\"}", - "data: {\"id\":\"chatcmpl-EBtoCBiLNVza52zxcVcNSx4yVdz3P\",\"object\":\"chat.completion.chunk\",\"created\":1786504844,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"C9WUYk5oWce\"}", - "data: {\"id\":\"chatcmpl-EBtoCBiLNVza52zxcVcNSx4yVdz3P\",\"object\":\"chat.completion.chunk\",\"created\":1786504844,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"city\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"8yFDeEuspJ\"}", - "data: {\"id\":\"chatcmpl-EBtoCBiLNVza52zxcVcNSx4yVdz3P\",\"object\":\"chat.completion.chunk\",\"created\":1786504844,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"hYvjgrAeg\"}", - "data: {\"id\":\"chatcmpl-EBtoCBiLNVza52zxcVcNSx4yVdz3P\",\"object\":\"chat.completion.chunk\",\"created\":1786504844,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Paris\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"IzInSLXSv\"}", - "data: {\"id\":\"chatcmpl-EBtoCBiLNVza52zxcVcNSx4yVdz3P\",\"object\":\"chat.completion.chunk\",\"created\":1786504844,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"obfuscation\":\"UElpLFcwh21\"}", - "data: {\"id\":\"chatcmpl-EBtoCBiLNVza52zxcVcNSx4yVdz3P\",\"object\":\"chat.completion.chunk\",\"created\":1786504844,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_9afcdcbeed\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"obfuscation\":\"2N\"}", - "data: [DONE]" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a29c58895cc08029-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Wed, 12 Aug 2026 03:20:44 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "465", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999990", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_e4f8d627b090474d9b8d57d8e4a96786" - }, - "status": 200, - "statusText": "OK" - } - } - ], - "meta": { - "createdAt": "2026-07-06T14:02:46.168Z" - } -} diff --git a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-auto-hook.span-tree.json b/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-auto-hook.span-tree.json deleted file mode 100644 index 3a9e3b97e..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-auto-hook.span-tree.json +++ /dev/null @@ -1,918 +0,0 @@ -{ - "span_tree": [ - { - "name": "ai-sdk-instrumentation-root", - "type": "task", - "children": [ - { - "name": "ai-sdk-generate-operation", - "children": [ - { - "name": "generateText", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 24, - "mode": { - "type": "regular" - }, - "prompt": [ - { - "content": [ - { - "text": "Reply with the single token PARIS and no punctuation.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "text": "PARIS", - "usage": { - "completionTokens": 2, - "promptTokens": 18 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 2, - "prompt_tokens": 18, - "tokens": 20 - } - } - ], - "input": { - "maxTokens": 24, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Reply with the single token PARIS and no punctuation.", - "temperature": 0 - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "rawResponse": {}, - "response": { - "messages": [ - { - "content": [ - { - "text": "PARIS", - "type": "text" - } - ], - "role": "assistant" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "responseMessages": "", - "steps": [ - { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "isContinued": false, - "response": { - "messages": [ - { - "content": [ - { - "text": "PARIS", - "type": "text" - } - ], - "role": "assistant" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "stepType": "initial", - "text": "PARIS", - "toolCalls": [], - "toolResults": [], - "usage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "warnings": [] - } - ], - "text": "PARIS", - "toolCalls": [], - "toolResults": [], - "usage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - } - ], - "metadata": { - "operation": "generate", - "testRunId": "" - } - }, - { - "name": "ai-sdk-stream-operation", - "children": [ - { - "name": "streamText", - "type": "function", - "children": [ - { - "name": "doStream", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "type": "regular" - }, - "prompt": [ - { - "content": [ - { - "text": "Count from 1 to 3 and include the words one two three.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "text": "One, two, three.", - "toolCalls": [], - "usage": { - "completionTokens": null, - "promptTokens": null - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Count from 1 to 3 and include the words one two three.", - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "rawResponse": {}, - "text": "One, two, three.", - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "time_to_first_token": 0 - } - } - ], - "metadata": { - "operation": "stream", - "testRunId": "" - } - }, - { - "name": "ai-sdk-embed-operation", - "children": [ - { - "name": "embed", - "type": "function", - "children": [], - "input": { - "model": { - "config": { - "provider": "openai.embedding" - }, - "modelId": "text-embedding-3-small", - "settings": {}, - "specificationVersion": "v1" - }, - "value": "Paris is the capital of France." - }, - "output": { - "embedding_length": 1536, - "usage": { - "tokens": 7 - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "text-embedding-3-small", - "provider": "openai.embedding" - }, - "metrics": { - "tokens": 7 - } - } - ], - "metadata": { - "operation": "embed", - "testRunId": "" - } - }, - { - "name": "ai-sdk-tool-operation", - "children": [ - { - "name": "generateText", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 128, - "mode": { - "toolChoice": { - "type": "auto" - }, - "tools": [ - { - "description": "Get the weather for a location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - }, - "type": "function" - } - ], - "type": "regular" - }, - "prompt": [ - { - "content": "You must inspect live weather via the provided get_weather tool before responding.", - "role": "system" - }, - { - "content": [ - { - "text": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "tool-calls", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "toolCalls": [ - { - "args": "{\"location\":\"Paris, France\"}", - "toolCallType": "function", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "tool-calls", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 16, - "prompt_tokens": 87, - "tokens": 103 - } - }, - { - "name": "get_weather", - "type": "tool", - "children": [], - "input": { - "location": "Paris, France" - }, - "output": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}" - } - ], - "input": { - "maxTokens": 128, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "system": "You must inspect live weather via the provided get_weather tool before responding.", - "temperature": 0, - "tools": { - "get_weather": { - "description": "Get the weather for a location", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - } - } - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "tool-calls", - "rawResponse": {}, - "response": { - "messages": [ - { - "content": [ - { - "text": "", - "type": "text" - }, - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather", - "type": "tool-result" - } - ], - "role": "tool" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "responseMessages": "", - "steps": [ - { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "tool-calls", - "isContinued": false, - "response": { - "messages": [ - { - "content": [ - { - "text": "", - "type": "text" - }, - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather", - "type": "tool-result" - } - ], - "role": "tool" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "stepType": "initial", - "text": "", - "toolCalls": [ - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "toolResults": [ - { - "args": { - "location": "Paris, France" - }, - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87, - "totalTokens": 103 - }, - "warnings": [] - } - ], - "text": "", - "toolCalls": [ - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "toolResults": [ - { - "args": { - "location": "Paris, France" - }, - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87, - "totalTokens": 103 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat", - "tools": { - "get_weather": { - "description": "Get the weather for a location", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - } - } - } - } - ], - "metadata": { - "operation": "tool", - "testRunId": "" - } - }, - { - "name": "ai-sdk-generate-object-operation", - "children": [ - { - "name": "generateObject", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Return JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json" - } - ], - "usage": { - "completionTokens": 5, - "promptTokens": 59 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 5, - "prompt_tokens": 59, - "tokens": 64 - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Return JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "usage": { - "completionTokens": 5, - "promptTokens": 59, - "totalTokens": 64 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - } - ], - "metadata": { - "operation": "generate-object", - "testRunId": "" - } - }, - { - "name": "ai-sdk-stream-object-operation", - "children": [ - { - "name": "streamObject", - "type": "function", - "children": [ - { - "name": "doStream", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Stream JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "text": "", - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json", - "type": "tool-call" - } - ], - "usage": { - "completionTokens": null, - "promptTokens": null - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Stream JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - }, - "output": { - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "time_to_first_token": 0 - } - } - ], - "metadata": { - "operation": "stream-object", - "testRunId": "" - } - } - ], - "metadata": { - "aiSdkVersion": "3.4.33", - "scenario": "ai-sdk-instrumentation", - "testRunId": "" - } - } - ] -} diff --git a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-auto-hook.span-tree.txt b/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-auto-hook.span-tree.txt deleted file mode 100644 index 2656453e0..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-auto-hook.span-tree.txt +++ /dev/null @@ -1,833 +0,0 @@ -span_tree: -└── ai-sdk-instrumentation-root [task] - metadata: { - "aiSdkVersion": "3.4.33", - "scenario": "ai-sdk-instrumentation", - "testRunId": "" - } - ├── ai-sdk-generate-operation - │ metadata: { - │ "operation": "generate", - │ "testRunId": "" - │ } - │ └── generateText [function] - │ input: { - │ "maxTokens": 24, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Reply with the single token PARIS and no punctuation.", - │ "temperature": 0 - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "rawResponse": {}, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "PARIS", - │ "type": "text" - │ } - │ ], - │ "role": "assistant" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "responseMessages": "", - │ "steps": [ - │ { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "isContinued": false, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "PARIS", - │ "type": "text" - │ } - │ ], - │ "role": "assistant" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "stepType": "initial", - │ "text": "PARIS", - │ "toolCalls": [], - │ "toolResults": [], - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "warnings": [] - │ } - │ ], - │ "text": "PARIS", - │ "toolCalls": [], - │ "toolResults": [], - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ └── doGenerate [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 24, - │ "mode": { - │ "type": "regular" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Reply with the single token PARIS and no punctuation.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "text": "PARIS", - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": 2, - │ "prompt_tokens": 18, - │ "tokens": 20 - │ } - ├── ai-sdk-stream-operation - │ metadata: { - │ "operation": "stream", - │ "testRunId": "" - │ } - │ └── streamText [function] - │ input: { - │ "maxTokens": 32, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Count from 1 to 3 and include the words one two three.", - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "rawResponse": {}, - │ "text": "One, two, three.", - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "time_to_first_token": 0 - │ } - │ └── doStream [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 32, - │ "mode": { - │ "type": "regular" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Count from 1 to 3 and include the words one two three.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "text": "One, two, three.", - │ "toolCalls": [], - │ "usage": { - │ "completionTokens": null, - │ "promptTokens": null - │ } - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": null, - │ "prompt_tokens": null, - │ "time_to_first_token": 0, - │ "tokens": null - │ } - ├── ai-sdk-embed-operation - │ metadata: { - │ "operation": "embed", - │ "testRunId": "" - │ } - │ └── embed [function] - │ input: { - │ "model": { - │ "config": { - │ "provider": "openai.embedding" - │ }, - │ "modelId": "text-embedding-3-small", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "value": "Paris is the capital of France." - │ } - │ output: { - │ "embedding_length": 1536, - │ "usage": { - │ "tokens": 7 - │ } - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "text-embedding-3-small", - │ "provider": "openai.embedding" - │ } - │ metrics: { - │ "tokens": 7 - │ } - ├── ai-sdk-tool-operation - │ metadata: { - │ "operation": "tool", - │ "testRunId": "" - │ } - │ └── generateText [function] - │ input: { - │ "maxTokens": 128, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - │ "system": "You must inspect live weather via the provided get_weather tool before responding.", - │ "temperature": 0, - │ "tools": { - │ "get_weather": { - │ "description": "Get the weather for a location", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ } - │ } - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "tool-calls", - │ "rawResponse": {}, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "", - │ "type": "text" - │ }, - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather", - │ "type": "tool-result" - │ } - │ ], - │ "role": "tool" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "responseMessages": "", - │ "steps": [ - │ { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "tool-calls", - │ "isContinued": false, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "", - │ "type": "text" - │ }, - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather", - │ "type": "tool-result" - │ } - │ ], - │ "role": "tool" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "stepType": "initial", - │ "text": "", - │ "toolCalls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "toolResults": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather" - │ } - │ ], - │ "usage": { - │ "completionTokens": 16, - │ "promptTokens": 87, - │ "totalTokens": 103 - │ }, - │ "warnings": [] - │ } - │ ], - │ "text": "", - │ "toolCalls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "toolResults": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather" - │ } - │ ], - │ "usage": { - │ "completionTokens": 16, - │ "promptTokens": 87, - │ "totalTokens": 103 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat", - │ "tools": { - │ "get_weather": { - │ "description": "Get the weather for a location", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ } - │ } - │ } - │ ├── doGenerate [llm] - │ │ input: { - │ │ "inputFormat": "prompt", - │ │ "maxRetries": 2, - │ │ "maxTokens": 128, - │ │ "mode": { - │ │ "toolChoice": { - │ │ "type": "auto" - │ │ }, - │ │ "tools": [ - │ │ { - │ │ "description": "Get the weather for a location", - │ │ "name": "get_weather", - │ │ "parameters": { - │ │ "$schema": "http://json-schema.org/draft-07/schema#", - │ │ "additionalProperties": false, - │ │ "properties": { - │ │ "location": { - │ │ "description": "The city and country", - │ │ "type": "string" - │ │ } - │ │ }, - │ │ "required": [ - │ │ "location" - │ │ ], - │ │ "type": "object" - │ │ }, - │ │ "type": "function" - │ │ } - │ │ ], - │ │ "type": "regular" - │ │ }, - │ │ "prompt": [ - │ │ { - │ │ "content": "You must inspect live weather via the provided get_weather tool before responding.", - │ │ "role": "system" - │ │ }, - │ │ { - │ │ "content": [ - │ │ { - │ │ "text": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - │ │ "type": "text" - │ │ } - │ │ ], - │ │ "role": "user" - │ │ } - │ │ ], - │ │ "temperature": 0 - │ │ } - │ │ output: { - │ │ "finishReason": "tool-calls", - │ │ "providerMetadata": { - │ │ "openai": { - │ │ "reasoningTokens": 0 - │ │ } - │ │ }, - │ │ "rawResponse": {}, - │ │ "response": { - │ │ "modelId": "gpt-4o-mini-2024-07-18", - │ │ "timestamp": "" - │ │ }, - │ │ "toolCalls": [ - │ │ { - │ │ "args": "{\"location\":\"Paris, France\"}", - │ │ "toolCallType": "function", - │ │ "toolName": "get_weather" - │ │ } - │ │ ], - │ │ "usage": { - │ │ "completionTokens": 16, - │ │ "promptTokens": 87 - │ │ }, - │ │ "warnings": [] - │ │ } - │ │ metadata: { - │ │ "braintrust": { - │ │ "integration_name": "ai-sdk", - │ │ "sdk_language": "typescript" - │ │ }, - │ │ "finish_reason": "tool-calls", - │ │ "model": "gpt-4o-mini-2024-07-18", - │ │ "provider": "openai.chat" - │ │ } - │ │ metrics: { - │ │ "completion_tokens": 16, - │ │ "prompt_tokens": 87, - │ │ "tokens": 103 - │ │ } - │ └── get_weather [tool] - │ input: { - │ "location": "Paris, France" - │ } - │ output: "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}" - ├── ai-sdk-generate-object-operation - │ metadata: { - │ "operation": "generate-object", - │ "testRunId": "" - │ } - │ └── generateObject [function] - │ input: { - │ "maxTokens": 32, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Return JSON with {\"city\":\"Paris\"}.", - │ "schema": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "city": { - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "city" - │ ], - │ "type": "object" - │ }, - │ "temperature": 0 - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "object": { - │ "city": "Paris" - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "usage": { - │ "completionTokens": 5, - │ "promptTokens": 59, - │ "totalTokens": 64 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ └── doGenerate [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 32, - │ "mode": { - │ "tool": { - │ "description": "Respond with a JSON object.", - │ "name": "json", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "city": { - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "city" - │ ], - │ "type": "object" - │ }, - │ "type": "function" - │ }, - │ "type": "object-tool" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Return JSON with {\"city\":\"Paris\"}.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "toolCalls": [ - │ { - │ "args": "{\"city\":\"Paris\"}", - │ "toolCallType": "function", - │ "toolName": "json" - │ } - │ ], - │ "usage": { - │ "completionTokens": 5, - │ "promptTokens": 59 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": 5, - │ "prompt_tokens": 59, - │ "tokens": 64 - │ } - └── ai-sdk-stream-object-operation - metadata: { - "operation": "stream-object", - "testRunId": "" - } - └── streamObject [function] - input: { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Stream JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - } - output: { - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "warnings": [] - } - metadata: { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - metrics: { - "time_to_first_token": 0 - } - └── doStream [llm] - input: { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Stream JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - } - output: { - "finishReason": "stop", - "text": "", - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json", - "type": "tool-call" - } - ], - "usage": { - "completionTokens": null, - "promptTokens": null - } - } - metadata: { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - metrics: { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } diff --git a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-auto-hook.span-tree.json b/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-auto-hook.span-tree.json deleted file mode 100644 index 3a9e3b97e..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-auto-hook.span-tree.json +++ /dev/null @@ -1,918 +0,0 @@ -{ - "span_tree": [ - { - "name": "ai-sdk-instrumentation-root", - "type": "task", - "children": [ - { - "name": "ai-sdk-generate-operation", - "children": [ - { - "name": "generateText", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 24, - "mode": { - "type": "regular" - }, - "prompt": [ - { - "content": [ - { - "text": "Reply with the single token PARIS and no punctuation.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "text": "PARIS", - "usage": { - "completionTokens": 2, - "promptTokens": 18 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 2, - "prompt_tokens": 18, - "tokens": 20 - } - } - ], - "input": { - "maxTokens": 24, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Reply with the single token PARIS and no punctuation.", - "temperature": 0 - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "rawResponse": {}, - "response": { - "messages": [ - { - "content": [ - { - "text": "PARIS", - "type": "text" - } - ], - "role": "assistant" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "responseMessages": "", - "steps": [ - { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "isContinued": false, - "response": { - "messages": [ - { - "content": [ - { - "text": "PARIS", - "type": "text" - } - ], - "role": "assistant" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "stepType": "initial", - "text": "PARIS", - "toolCalls": [], - "toolResults": [], - "usage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "warnings": [] - } - ], - "text": "PARIS", - "toolCalls": [], - "toolResults": [], - "usage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - } - ], - "metadata": { - "operation": "generate", - "testRunId": "" - } - }, - { - "name": "ai-sdk-stream-operation", - "children": [ - { - "name": "streamText", - "type": "function", - "children": [ - { - "name": "doStream", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "type": "regular" - }, - "prompt": [ - { - "content": [ - { - "text": "Count from 1 to 3 and include the words one two three.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "text": "One, two, three.", - "toolCalls": [], - "usage": { - "completionTokens": null, - "promptTokens": null - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Count from 1 to 3 and include the words one two three.", - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "rawResponse": {}, - "text": "One, two, three.", - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "time_to_first_token": 0 - } - } - ], - "metadata": { - "operation": "stream", - "testRunId": "" - } - }, - { - "name": "ai-sdk-embed-operation", - "children": [ - { - "name": "embed", - "type": "function", - "children": [], - "input": { - "model": { - "config": { - "provider": "openai.embedding" - }, - "modelId": "text-embedding-3-small", - "settings": {}, - "specificationVersion": "v1" - }, - "value": "Paris is the capital of France." - }, - "output": { - "embedding_length": 1536, - "usage": { - "tokens": 7 - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "text-embedding-3-small", - "provider": "openai.embedding" - }, - "metrics": { - "tokens": 7 - } - } - ], - "metadata": { - "operation": "embed", - "testRunId": "" - } - }, - { - "name": "ai-sdk-tool-operation", - "children": [ - { - "name": "generateText", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 128, - "mode": { - "toolChoice": { - "type": "auto" - }, - "tools": [ - { - "description": "Get the weather for a location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - }, - "type": "function" - } - ], - "type": "regular" - }, - "prompt": [ - { - "content": "You must inspect live weather via the provided get_weather tool before responding.", - "role": "system" - }, - { - "content": [ - { - "text": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "tool-calls", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "toolCalls": [ - { - "args": "{\"location\":\"Paris, France\"}", - "toolCallType": "function", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "tool-calls", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 16, - "prompt_tokens": 87, - "tokens": 103 - } - }, - { - "name": "get_weather", - "type": "tool", - "children": [], - "input": { - "location": "Paris, France" - }, - "output": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}" - } - ], - "input": { - "maxTokens": 128, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "system": "You must inspect live weather via the provided get_weather tool before responding.", - "temperature": 0, - "tools": { - "get_weather": { - "description": "Get the weather for a location", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - } - } - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "tool-calls", - "rawResponse": {}, - "response": { - "messages": [ - { - "content": [ - { - "text": "", - "type": "text" - }, - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather", - "type": "tool-result" - } - ], - "role": "tool" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "responseMessages": "", - "steps": [ - { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "tool-calls", - "isContinued": false, - "response": { - "messages": [ - { - "content": [ - { - "text": "", - "type": "text" - }, - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather", - "type": "tool-result" - } - ], - "role": "tool" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "stepType": "initial", - "text": "", - "toolCalls": [ - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "toolResults": [ - { - "args": { - "location": "Paris, France" - }, - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87, - "totalTokens": 103 - }, - "warnings": [] - } - ], - "text": "", - "toolCalls": [ - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "toolResults": [ - { - "args": { - "location": "Paris, France" - }, - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87, - "totalTokens": 103 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat", - "tools": { - "get_weather": { - "description": "Get the weather for a location", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - } - } - } - } - ], - "metadata": { - "operation": "tool", - "testRunId": "" - } - }, - { - "name": "ai-sdk-generate-object-operation", - "children": [ - { - "name": "generateObject", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Return JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json" - } - ], - "usage": { - "completionTokens": 5, - "promptTokens": 59 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 5, - "prompt_tokens": 59, - "tokens": 64 - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Return JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "usage": { - "completionTokens": 5, - "promptTokens": 59, - "totalTokens": 64 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - } - ], - "metadata": { - "operation": "generate-object", - "testRunId": "" - } - }, - { - "name": "ai-sdk-stream-object-operation", - "children": [ - { - "name": "streamObject", - "type": "function", - "children": [ - { - "name": "doStream", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Stream JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "text": "", - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json", - "type": "tool-call" - } - ], - "usage": { - "completionTokens": null, - "promptTokens": null - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Stream JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - }, - "output": { - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "time_to_first_token": 0 - } - } - ], - "metadata": { - "operation": "stream-object", - "testRunId": "" - } - } - ], - "metadata": { - "aiSdkVersion": "3.4.33", - "scenario": "ai-sdk-instrumentation", - "testRunId": "" - } - } - ] -} diff --git a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-auto-hook.span-tree.txt b/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-auto-hook.span-tree.txt deleted file mode 100644 index 2656453e0..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-auto-hook.span-tree.txt +++ /dev/null @@ -1,833 +0,0 @@ -span_tree: -└── ai-sdk-instrumentation-root [task] - metadata: { - "aiSdkVersion": "3.4.33", - "scenario": "ai-sdk-instrumentation", - "testRunId": "" - } - ├── ai-sdk-generate-operation - │ metadata: { - │ "operation": "generate", - │ "testRunId": "" - │ } - │ └── generateText [function] - │ input: { - │ "maxTokens": 24, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Reply with the single token PARIS and no punctuation.", - │ "temperature": 0 - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "rawResponse": {}, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "PARIS", - │ "type": "text" - │ } - │ ], - │ "role": "assistant" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "responseMessages": "", - │ "steps": [ - │ { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "isContinued": false, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "PARIS", - │ "type": "text" - │ } - │ ], - │ "role": "assistant" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "stepType": "initial", - │ "text": "PARIS", - │ "toolCalls": [], - │ "toolResults": [], - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "warnings": [] - │ } - │ ], - │ "text": "PARIS", - │ "toolCalls": [], - │ "toolResults": [], - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ └── doGenerate [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 24, - │ "mode": { - │ "type": "regular" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Reply with the single token PARIS and no punctuation.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "text": "PARIS", - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": 2, - │ "prompt_tokens": 18, - │ "tokens": 20 - │ } - ├── ai-sdk-stream-operation - │ metadata: { - │ "operation": "stream", - │ "testRunId": "" - │ } - │ └── streamText [function] - │ input: { - │ "maxTokens": 32, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Count from 1 to 3 and include the words one two three.", - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "rawResponse": {}, - │ "text": "One, two, three.", - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "time_to_first_token": 0 - │ } - │ └── doStream [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 32, - │ "mode": { - │ "type": "regular" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Count from 1 to 3 and include the words one two three.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "text": "One, two, three.", - │ "toolCalls": [], - │ "usage": { - │ "completionTokens": null, - │ "promptTokens": null - │ } - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": null, - │ "prompt_tokens": null, - │ "time_to_first_token": 0, - │ "tokens": null - │ } - ├── ai-sdk-embed-operation - │ metadata: { - │ "operation": "embed", - │ "testRunId": "" - │ } - │ └── embed [function] - │ input: { - │ "model": { - │ "config": { - │ "provider": "openai.embedding" - │ }, - │ "modelId": "text-embedding-3-small", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "value": "Paris is the capital of France." - │ } - │ output: { - │ "embedding_length": 1536, - │ "usage": { - │ "tokens": 7 - │ } - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "text-embedding-3-small", - │ "provider": "openai.embedding" - │ } - │ metrics: { - │ "tokens": 7 - │ } - ├── ai-sdk-tool-operation - │ metadata: { - │ "operation": "tool", - │ "testRunId": "" - │ } - │ └── generateText [function] - │ input: { - │ "maxTokens": 128, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - │ "system": "You must inspect live weather via the provided get_weather tool before responding.", - │ "temperature": 0, - │ "tools": { - │ "get_weather": { - │ "description": "Get the weather for a location", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ } - │ } - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "tool-calls", - │ "rawResponse": {}, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "", - │ "type": "text" - │ }, - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather", - │ "type": "tool-result" - │ } - │ ], - │ "role": "tool" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "responseMessages": "", - │ "steps": [ - │ { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "tool-calls", - │ "isContinued": false, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "", - │ "type": "text" - │ }, - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather", - │ "type": "tool-result" - │ } - │ ], - │ "role": "tool" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "stepType": "initial", - │ "text": "", - │ "toolCalls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "toolResults": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather" - │ } - │ ], - │ "usage": { - │ "completionTokens": 16, - │ "promptTokens": 87, - │ "totalTokens": 103 - │ }, - │ "warnings": [] - │ } - │ ], - │ "text": "", - │ "toolCalls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "toolResults": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather" - │ } - │ ], - │ "usage": { - │ "completionTokens": 16, - │ "promptTokens": 87, - │ "totalTokens": 103 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat", - │ "tools": { - │ "get_weather": { - │ "description": "Get the weather for a location", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ } - │ } - │ } - │ ├── doGenerate [llm] - │ │ input: { - │ │ "inputFormat": "prompt", - │ │ "maxRetries": 2, - │ │ "maxTokens": 128, - │ │ "mode": { - │ │ "toolChoice": { - │ │ "type": "auto" - │ │ }, - │ │ "tools": [ - │ │ { - │ │ "description": "Get the weather for a location", - │ │ "name": "get_weather", - │ │ "parameters": { - │ │ "$schema": "http://json-schema.org/draft-07/schema#", - │ │ "additionalProperties": false, - │ │ "properties": { - │ │ "location": { - │ │ "description": "The city and country", - │ │ "type": "string" - │ │ } - │ │ }, - │ │ "required": [ - │ │ "location" - │ │ ], - │ │ "type": "object" - │ │ }, - │ │ "type": "function" - │ │ } - │ │ ], - │ │ "type": "regular" - │ │ }, - │ │ "prompt": [ - │ │ { - │ │ "content": "You must inspect live weather via the provided get_weather tool before responding.", - │ │ "role": "system" - │ │ }, - │ │ { - │ │ "content": [ - │ │ { - │ │ "text": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - │ │ "type": "text" - │ │ } - │ │ ], - │ │ "role": "user" - │ │ } - │ │ ], - │ │ "temperature": 0 - │ │ } - │ │ output: { - │ │ "finishReason": "tool-calls", - │ │ "providerMetadata": { - │ │ "openai": { - │ │ "reasoningTokens": 0 - │ │ } - │ │ }, - │ │ "rawResponse": {}, - │ │ "response": { - │ │ "modelId": "gpt-4o-mini-2024-07-18", - │ │ "timestamp": "" - │ │ }, - │ │ "toolCalls": [ - │ │ { - │ │ "args": "{\"location\":\"Paris, France\"}", - │ │ "toolCallType": "function", - │ │ "toolName": "get_weather" - │ │ } - │ │ ], - │ │ "usage": { - │ │ "completionTokens": 16, - │ │ "promptTokens": 87 - │ │ }, - │ │ "warnings": [] - │ │ } - │ │ metadata: { - │ │ "braintrust": { - │ │ "integration_name": "ai-sdk", - │ │ "sdk_language": "typescript" - │ │ }, - │ │ "finish_reason": "tool-calls", - │ │ "model": "gpt-4o-mini-2024-07-18", - │ │ "provider": "openai.chat" - │ │ } - │ │ metrics: { - │ │ "completion_tokens": 16, - │ │ "prompt_tokens": 87, - │ │ "tokens": 103 - │ │ } - │ └── get_weather [tool] - │ input: { - │ "location": "Paris, France" - │ } - │ output: "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}" - ├── ai-sdk-generate-object-operation - │ metadata: { - │ "operation": "generate-object", - │ "testRunId": "" - │ } - │ └── generateObject [function] - │ input: { - │ "maxTokens": 32, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Return JSON with {\"city\":\"Paris\"}.", - │ "schema": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "city": { - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "city" - │ ], - │ "type": "object" - │ }, - │ "temperature": 0 - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "object": { - │ "city": "Paris" - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "usage": { - │ "completionTokens": 5, - │ "promptTokens": 59, - │ "totalTokens": 64 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ └── doGenerate [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 32, - │ "mode": { - │ "tool": { - │ "description": "Respond with a JSON object.", - │ "name": "json", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "city": { - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "city" - │ ], - │ "type": "object" - │ }, - │ "type": "function" - │ }, - │ "type": "object-tool" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Return JSON with {\"city\":\"Paris\"}.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "toolCalls": [ - │ { - │ "args": "{\"city\":\"Paris\"}", - │ "toolCallType": "function", - │ "toolName": "json" - │ } - │ ], - │ "usage": { - │ "completionTokens": 5, - │ "promptTokens": 59 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": 5, - │ "prompt_tokens": 59, - │ "tokens": 64 - │ } - └── ai-sdk-stream-object-operation - metadata: { - "operation": "stream-object", - "testRunId": "" - } - └── streamObject [function] - input: { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Stream JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - } - output: { - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "warnings": [] - } - metadata: { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - metrics: { - "time_to_first_token": 0 - } - └── doStream [llm] - input: { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Stream JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - } - output: { - "finishReason": "stop", - "text": "", - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json", - "type": "tool-call" - } - ], - "usage": { - "completionTokens": null, - "promptTokens": null - } - } - metadata: { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - metrics: { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } diff --git a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-wrapped.span-tree.json b/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-wrapped.span-tree.json deleted file mode 100644 index 3a9e3b97e..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-wrapped.span-tree.json +++ /dev/null @@ -1,918 +0,0 @@ -{ - "span_tree": [ - { - "name": "ai-sdk-instrumentation-root", - "type": "task", - "children": [ - { - "name": "ai-sdk-generate-operation", - "children": [ - { - "name": "generateText", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 24, - "mode": { - "type": "regular" - }, - "prompt": [ - { - "content": [ - { - "text": "Reply with the single token PARIS and no punctuation.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "text": "PARIS", - "usage": { - "completionTokens": 2, - "promptTokens": 18 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 2, - "prompt_tokens": 18, - "tokens": 20 - } - } - ], - "input": { - "maxTokens": 24, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Reply with the single token PARIS and no punctuation.", - "temperature": 0 - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "rawResponse": {}, - "response": { - "messages": [ - { - "content": [ - { - "text": "PARIS", - "type": "text" - } - ], - "role": "assistant" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "responseMessages": "", - "steps": [ - { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "isContinued": false, - "response": { - "messages": [ - { - "content": [ - { - "text": "PARIS", - "type": "text" - } - ], - "role": "assistant" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "stepType": "initial", - "text": "PARIS", - "toolCalls": [], - "toolResults": [], - "usage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "warnings": [] - } - ], - "text": "PARIS", - "toolCalls": [], - "toolResults": [], - "usage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - } - ], - "metadata": { - "operation": "generate", - "testRunId": "" - } - }, - { - "name": "ai-sdk-stream-operation", - "children": [ - { - "name": "streamText", - "type": "function", - "children": [ - { - "name": "doStream", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "type": "regular" - }, - "prompt": [ - { - "content": [ - { - "text": "Count from 1 to 3 and include the words one two three.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "text": "One, two, three.", - "toolCalls": [], - "usage": { - "completionTokens": null, - "promptTokens": null - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Count from 1 to 3 and include the words one two three.", - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "rawResponse": {}, - "text": "One, two, three.", - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "time_to_first_token": 0 - } - } - ], - "metadata": { - "operation": "stream", - "testRunId": "" - } - }, - { - "name": "ai-sdk-embed-operation", - "children": [ - { - "name": "embed", - "type": "function", - "children": [], - "input": { - "model": { - "config": { - "provider": "openai.embedding" - }, - "modelId": "text-embedding-3-small", - "settings": {}, - "specificationVersion": "v1" - }, - "value": "Paris is the capital of France." - }, - "output": { - "embedding_length": 1536, - "usage": { - "tokens": 7 - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "text-embedding-3-small", - "provider": "openai.embedding" - }, - "metrics": { - "tokens": 7 - } - } - ], - "metadata": { - "operation": "embed", - "testRunId": "" - } - }, - { - "name": "ai-sdk-tool-operation", - "children": [ - { - "name": "generateText", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 128, - "mode": { - "toolChoice": { - "type": "auto" - }, - "tools": [ - { - "description": "Get the weather for a location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - }, - "type": "function" - } - ], - "type": "regular" - }, - "prompt": [ - { - "content": "You must inspect live weather via the provided get_weather tool before responding.", - "role": "system" - }, - { - "content": [ - { - "text": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "tool-calls", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "toolCalls": [ - { - "args": "{\"location\":\"Paris, France\"}", - "toolCallType": "function", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "tool-calls", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 16, - "prompt_tokens": 87, - "tokens": 103 - } - }, - { - "name": "get_weather", - "type": "tool", - "children": [], - "input": { - "location": "Paris, France" - }, - "output": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}" - } - ], - "input": { - "maxTokens": 128, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "system": "You must inspect live weather via the provided get_weather tool before responding.", - "temperature": 0, - "tools": { - "get_weather": { - "description": "Get the weather for a location", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - } - } - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "tool-calls", - "rawResponse": {}, - "response": { - "messages": [ - { - "content": [ - { - "text": "", - "type": "text" - }, - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather", - "type": "tool-result" - } - ], - "role": "tool" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "responseMessages": "", - "steps": [ - { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "tool-calls", - "isContinued": false, - "response": { - "messages": [ - { - "content": [ - { - "text": "", - "type": "text" - }, - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather", - "type": "tool-result" - } - ], - "role": "tool" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "stepType": "initial", - "text": "", - "toolCalls": [ - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "toolResults": [ - { - "args": { - "location": "Paris, France" - }, - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87, - "totalTokens": 103 - }, - "warnings": [] - } - ], - "text": "", - "toolCalls": [ - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "toolResults": [ - { - "args": { - "location": "Paris, France" - }, - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87, - "totalTokens": 103 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat", - "tools": { - "get_weather": { - "description": "Get the weather for a location", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - } - } - } - } - ], - "metadata": { - "operation": "tool", - "testRunId": "" - } - }, - { - "name": "ai-sdk-generate-object-operation", - "children": [ - { - "name": "generateObject", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Return JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json" - } - ], - "usage": { - "completionTokens": 5, - "promptTokens": 59 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 5, - "prompt_tokens": 59, - "tokens": 64 - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Return JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "usage": { - "completionTokens": 5, - "promptTokens": 59, - "totalTokens": 64 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - } - ], - "metadata": { - "operation": "generate-object", - "testRunId": "" - } - }, - { - "name": "ai-sdk-stream-object-operation", - "children": [ - { - "name": "streamObject", - "type": "function", - "children": [ - { - "name": "doStream", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Stream JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "text": "", - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json", - "type": "tool-call" - } - ], - "usage": { - "completionTokens": null, - "promptTokens": null - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Stream JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - }, - "output": { - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "time_to_first_token": 0 - } - } - ], - "metadata": { - "operation": "stream-object", - "testRunId": "" - } - } - ], - "metadata": { - "aiSdkVersion": "3.4.33", - "scenario": "ai-sdk-instrumentation", - "testRunId": "" - } - } - ] -} diff --git a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-wrapped.span-tree.txt b/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-wrapped.span-tree.txt deleted file mode 100644 index 2656453e0..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-latest-wrapped.span-tree.txt +++ /dev/null @@ -1,833 +0,0 @@ -span_tree: -└── ai-sdk-instrumentation-root [task] - metadata: { - "aiSdkVersion": "3.4.33", - "scenario": "ai-sdk-instrumentation", - "testRunId": "" - } - ├── ai-sdk-generate-operation - │ metadata: { - │ "operation": "generate", - │ "testRunId": "" - │ } - │ └── generateText [function] - │ input: { - │ "maxTokens": 24, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Reply with the single token PARIS and no punctuation.", - │ "temperature": 0 - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "rawResponse": {}, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "PARIS", - │ "type": "text" - │ } - │ ], - │ "role": "assistant" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "responseMessages": "", - │ "steps": [ - │ { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "isContinued": false, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "PARIS", - │ "type": "text" - │ } - │ ], - │ "role": "assistant" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "stepType": "initial", - │ "text": "PARIS", - │ "toolCalls": [], - │ "toolResults": [], - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "warnings": [] - │ } - │ ], - │ "text": "PARIS", - │ "toolCalls": [], - │ "toolResults": [], - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ └── doGenerate [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 24, - │ "mode": { - │ "type": "regular" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Reply with the single token PARIS and no punctuation.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "text": "PARIS", - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": 2, - │ "prompt_tokens": 18, - │ "tokens": 20 - │ } - ├── ai-sdk-stream-operation - │ metadata: { - │ "operation": "stream", - │ "testRunId": "" - │ } - │ └── streamText [function] - │ input: { - │ "maxTokens": 32, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Count from 1 to 3 and include the words one two three.", - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "rawResponse": {}, - │ "text": "One, two, three.", - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "time_to_first_token": 0 - │ } - │ └── doStream [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 32, - │ "mode": { - │ "type": "regular" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Count from 1 to 3 and include the words one two three.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "text": "One, two, three.", - │ "toolCalls": [], - │ "usage": { - │ "completionTokens": null, - │ "promptTokens": null - │ } - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": null, - │ "prompt_tokens": null, - │ "time_to_first_token": 0, - │ "tokens": null - │ } - ├── ai-sdk-embed-operation - │ metadata: { - │ "operation": "embed", - │ "testRunId": "" - │ } - │ └── embed [function] - │ input: { - │ "model": { - │ "config": { - │ "provider": "openai.embedding" - │ }, - │ "modelId": "text-embedding-3-small", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "value": "Paris is the capital of France." - │ } - │ output: { - │ "embedding_length": 1536, - │ "usage": { - │ "tokens": 7 - │ } - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "text-embedding-3-small", - │ "provider": "openai.embedding" - │ } - │ metrics: { - │ "tokens": 7 - │ } - ├── ai-sdk-tool-operation - │ metadata: { - │ "operation": "tool", - │ "testRunId": "" - │ } - │ └── generateText [function] - │ input: { - │ "maxTokens": 128, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - │ "system": "You must inspect live weather via the provided get_weather tool before responding.", - │ "temperature": 0, - │ "tools": { - │ "get_weather": { - │ "description": "Get the weather for a location", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ } - │ } - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "tool-calls", - │ "rawResponse": {}, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "", - │ "type": "text" - │ }, - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather", - │ "type": "tool-result" - │ } - │ ], - │ "role": "tool" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "responseMessages": "", - │ "steps": [ - │ { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "tool-calls", - │ "isContinued": false, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "", - │ "type": "text" - │ }, - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather", - │ "type": "tool-result" - │ } - │ ], - │ "role": "tool" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "stepType": "initial", - │ "text": "", - │ "toolCalls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "toolResults": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather" - │ } - │ ], - │ "usage": { - │ "completionTokens": 16, - │ "promptTokens": 87, - │ "totalTokens": 103 - │ }, - │ "warnings": [] - │ } - │ ], - │ "text": "", - │ "toolCalls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "toolResults": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather" - │ } - │ ], - │ "usage": { - │ "completionTokens": 16, - │ "promptTokens": 87, - │ "totalTokens": 103 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat", - │ "tools": { - │ "get_weather": { - │ "description": "Get the weather for a location", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ } - │ } - │ } - │ ├── doGenerate [llm] - │ │ input: { - │ │ "inputFormat": "prompt", - │ │ "maxRetries": 2, - │ │ "maxTokens": 128, - │ │ "mode": { - │ │ "toolChoice": { - │ │ "type": "auto" - │ │ }, - │ │ "tools": [ - │ │ { - │ │ "description": "Get the weather for a location", - │ │ "name": "get_weather", - │ │ "parameters": { - │ │ "$schema": "http://json-schema.org/draft-07/schema#", - │ │ "additionalProperties": false, - │ │ "properties": { - │ │ "location": { - │ │ "description": "The city and country", - │ │ "type": "string" - │ │ } - │ │ }, - │ │ "required": [ - │ │ "location" - │ │ ], - │ │ "type": "object" - │ │ }, - │ │ "type": "function" - │ │ } - │ │ ], - │ │ "type": "regular" - │ │ }, - │ │ "prompt": [ - │ │ { - │ │ "content": "You must inspect live weather via the provided get_weather tool before responding.", - │ │ "role": "system" - │ │ }, - │ │ { - │ │ "content": [ - │ │ { - │ │ "text": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - │ │ "type": "text" - │ │ } - │ │ ], - │ │ "role": "user" - │ │ } - │ │ ], - │ │ "temperature": 0 - │ │ } - │ │ output: { - │ │ "finishReason": "tool-calls", - │ │ "providerMetadata": { - │ │ "openai": { - │ │ "reasoningTokens": 0 - │ │ } - │ │ }, - │ │ "rawResponse": {}, - │ │ "response": { - │ │ "modelId": "gpt-4o-mini-2024-07-18", - │ │ "timestamp": "" - │ │ }, - │ │ "toolCalls": [ - │ │ { - │ │ "args": "{\"location\":\"Paris, France\"}", - │ │ "toolCallType": "function", - │ │ "toolName": "get_weather" - │ │ } - │ │ ], - │ │ "usage": { - │ │ "completionTokens": 16, - │ │ "promptTokens": 87 - │ │ }, - │ │ "warnings": [] - │ │ } - │ │ metadata: { - │ │ "braintrust": { - │ │ "integration_name": "ai-sdk", - │ │ "sdk_language": "typescript" - │ │ }, - │ │ "finish_reason": "tool-calls", - │ │ "model": "gpt-4o-mini-2024-07-18", - │ │ "provider": "openai.chat" - │ │ } - │ │ metrics: { - │ │ "completion_tokens": 16, - │ │ "prompt_tokens": 87, - │ │ "tokens": 103 - │ │ } - │ └── get_weather [tool] - │ input: { - │ "location": "Paris, France" - │ } - │ output: "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}" - ├── ai-sdk-generate-object-operation - │ metadata: { - │ "operation": "generate-object", - │ "testRunId": "" - │ } - │ └── generateObject [function] - │ input: { - │ "maxTokens": 32, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Return JSON with {\"city\":\"Paris\"}.", - │ "schema": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "city": { - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "city" - │ ], - │ "type": "object" - │ }, - │ "temperature": 0 - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "object": { - │ "city": "Paris" - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "usage": { - │ "completionTokens": 5, - │ "promptTokens": 59, - │ "totalTokens": 64 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ └── doGenerate [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 32, - │ "mode": { - │ "tool": { - │ "description": "Respond with a JSON object.", - │ "name": "json", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "city": { - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "city" - │ ], - │ "type": "object" - │ }, - │ "type": "function" - │ }, - │ "type": "object-tool" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Return JSON with {\"city\":\"Paris\"}.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "toolCalls": [ - │ { - │ "args": "{\"city\":\"Paris\"}", - │ "toolCallType": "function", - │ "toolName": "json" - │ } - │ ], - │ "usage": { - │ "completionTokens": 5, - │ "promptTokens": 59 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": 5, - │ "prompt_tokens": 59, - │ "tokens": 64 - │ } - └── ai-sdk-stream-object-operation - metadata: { - "operation": "stream-object", - "testRunId": "" - } - └── streamObject [function] - input: { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Stream JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - } - output: { - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "warnings": [] - } - metadata: { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - metrics: { - "time_to_first_token": 0 - } - └── doStream [llm] - input: { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Stream JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - } - output: { - "finishReason": "stop", - "text": "", - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json", - "type": "tool-call" - } - ], - "usage": { - "completionTokens": null, - "promptTokens": null - } - } - metadata: { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - metrics: { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } diff --git a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-wrapped.span-tree.json b/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-wrapped.span-tree.json deleted file mode 100644 index 3a9e3b97e..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-wrapped.span-tree.json +++ /dev/null @@ -1,918 +0,0 @@ -{ - "span_tree": [ - { - "name": "ai-sdk-instrumentation-root", - "type": "task", - "children": [ - { - "name": "ai-sdk-generate-operation", - "children": [ - { - "name": "generateText", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 24, - "mode": { - "type": "regular" - }, - "prompt": [ - { - "content": [ - { - "text": "Reply with the single token PARIS and no punctuation.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "text": "PARIS", - "usage": { - "completionTokens": 2, - "promptTokens": 18 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 2, - "prompt_tokens": 18, - "tokens": 20 - } - } - ], - "input": { - "maxTokens": 24, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Reply with the single token PARIS and no punctuation.", - "temperature": 0 - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "rawResponse": {}, - "response": { - "messages": [ - { - "content": [ - { - "text": "PARIS", - "type": "text" - } - ], - "role": "assistant" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "responseMessages": "", - "steps": [ - { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "isContinued": false, - "response": { - "messages": [ - { - "content": [ - { - "text": "PARIS", - "type": "text" - } - ], - "role": "assistant" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "stepType": "initial", - "text": "PARIS", - "toolCalls": [], - "toolResults": [], - "usage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "warnings": [] - } - ], - "text": "PARIS", - "toolCalls": [], - "toolResults": [], - "usage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - } - ], - "metadata": { - "operation": "generate", - "testRunId": "" - } - }, - { - "name": "ai-sdk-stream-operation", - "children": [ - { - "name": "streamText", - "type": "function", - "children": [ - { - "name": "doStream", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "type": "regular" - }, - "prompt": [ - { - "content": [ - { - "text": "Count from 1 to 3 and include the words one two three.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "text": "One, two, three.", - "toolCalls": [], - "usage": { - "completionTokens": null, - "promptTokens": null - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Count from 1 to 3 and include the words one two three.", - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "rawResponse": {}, - "text": "One, two, three.", - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "time_to_first_token": 0 - } - } - ], - "metadata": { - "operation": "stream", - "testRunId": "" - } - }, - { - "name": "ai-sdk-embed-operation", - "children": [ - { - "name": "embed", - "type": "function", - "children": [], - "input": { - "model": { - "config": { - "provider": "openai.embedding" - }, - "modelId": "text-embedding-3-small", - "settings": {}, - "specificationVersion": "v1" - }, - "value": "Paris is the capital of France." - }, - "output": { - "embedding_length": 1536, - "usage": { - "tokens": 7 - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "text-embedding-3-small", - "provider": "openai.embedding" - }, - "metrics": { - "tokens": 7 - } - } - ], - "metadata": { - "operation": "embed", - "testRunId": "" - } - }, - { - "name": "ai-sdk-tool-operation", - "children": [ - { - "name": "generateText", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 128, - "mode": { - "toolChoice": { - "type": "auto" - }, - "tools": [ - { - "description": "Get the weather for a location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - }, - "type": "function" - } - ], - "type": "regular" - }, - "prompt": [ - { - "content": "You must inspect live weather via the provided get_weather tool before responding.", - "role": "system" - }, - { - "content": [ - { - "text": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "tool-calls", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "toolCalls": [ - { - "args": "{\"location\":\"Paris, France\"}", - "toolCallType": "function", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "tool-calls", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 16, - "prompt_tokens": 87, - "tokens": 103 - } - }, - { - "name": "get_weather", - "type": "tool", - "children": [], - "input": { - "location": "Paris, France" - }, - "output": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}" - } - ], - "input": { - "maxTokens": 128, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - "system": "You must inspect live weather via the provided get_weather tool before responding.", - "temperature": 0, - "tools": { - "get_weather": { - "description": "Get the weather for a location", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - } - } - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "tool-calls", - "rawResponse": {}, - "response": { - "messages": [ - { - "content": [ - { - "text": "", - "type": "text" - }, - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather", - "type": "tool-result" - } - ], - "role": "tool" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "responseMessages": "", - "steps": [ - { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "tool-calls", - "isContinued": false, - "response": { - "messages": [ - { - "content": [ - { - "text": "", - "type": "text" - }, - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "role": "assistant" - }, - { - "content": [ - { - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather", - "type": "tool-result" - } - ], - "role": "tool" - } - ], - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "stepType": "initial", - "text": "", - "toolCalls": [ - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "toolResults": [ - { - "args": { - "location": "Paris, France" - }, - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87, - "totalTokens": 103 - }, - "warnings": [] - } - ], - "text": "", - "toolCalls": [ - { - "args": { - "location": "Paris, France" - }, - "toolName": "get_weather", - "type": "tool-call" - } - ], - "toolResults": [ - { - "args": { - "location": "Paris, France" - }, - "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - "toolName": "get_weather" - } - ], - "usage": { - "completionTokens": 16, - "promptTokens": 87, - "totalTokens": 103 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat", - "tools": { - "get_weather": { - "description": "Get the weather for a location", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - } - } - } - } - ], - "metadata": { - "operation": "tool", - "testRunId": "" - } - }, - { - "name": "ai-sdk-generate-object-operation", - "children": [ - { - "name": "generateObject", - "type": "function", - "children": [ - { - "name": "doGenerate", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Return JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json" - } - ], - "usage": { - "completionTokens": 5, - "promptTokens": 59 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": 5, - "prompt_tokens": 59, - "tokens": 64 - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Return JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - }, - "output": { - "experimental_providerMetadata": { - "openai": { - "reasoningTokens": 0 - } - }, - "finishReason": "stop", - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "response": { - "modelId": "gpt-4o-mini-2024-07-18", - "timestamp": "" - }, - "usage": { - "completionTokens": 5, - "promptTokens": 59, - "totalTokens": 64 - }, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - } - ], - "metadata": { - "operation": "generate-object", - "testRunId": "" - } - }, - { - "name": "ai-sdk-stream-object-operation", - "children": [ - { - "name": "streamObject", - "type": "function", - "children": [ - { - "name": "doStream", - "type": "llm", - "children": [], - "input": { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Stream JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - }, - "output": { - "finishReason": "stop", - "text": "", - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json", - "type": "tool-call" - } - ], - "usage": { - "completionTokens": null, - "promptTokens": null - } - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } - } - ], - "input": { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Stream JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - }, - "output": { - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "warnings": [] - }, - "metadata": { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - }, - "metrics": { - "time_to_first_token": 0 - } - } - ], - "metadata": { - "operation": "stream-object", - "testRunId": "" - } - } - ], - "metadata": { - "aiSdkVersion": "3.4.33", - "scenario": "ai-sdk-instrumentation", - "testRunId": "" - } - } - ] -} diff --git a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-wrapped.span-tree.txt b/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-wrapped.span-tree.txt deleted file mode 100644 index 2656453e0..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/__snapshots__/ai-sdk-v3-wrapped.span-tree.txt +++ /dev/null @@ -1,833 +0,0 @@ -span_tree: -└── ai-sdk-instrumentation-root [task] - metadata: { - "aiSdkVersion": "3.4.33", - "scenario": "ai-sdk-instrumentation", - "testRunId": "" - } - ├── ai-sdk-generate-operation - │ metadata: { - │ "operation": "generate", - │ "testRunId": "" - │ } - │ └── generateText [function] - │ input: { - │ "maxTokens": 24, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Reply with the single token PARIS and no punctuation.", - │ "temperature": 0 - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "rawResponse": {}, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "PARIS", - │ "type": "text" - │ } - │ ], - │ "role": "assistant" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "responseMessages": "", - │ "steps": [ - │ { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "isContinued": false, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "PARIS", - │ "type": "text" - │ } - │ ], - │ "role": "assistant" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "stepType": "initial", - │ "text": "PARIS", - │ "toolCalls": [], - │ "toolResults": [], - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "warnings": [] - │ } - │ ], - │ "text": "PARIS", - │ "toolCalls": [], - │ "toolResults": [], - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ └── doGenerate [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 24, - │ "mode": { - │ "type": "regular" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Reply with the single token PARIS and no punctuation.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "text": "PARIS", - │ "usage": { - │ "completionTokens": 2, - │ "promptTokens": 18 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": 2, - │ "prompt_tokens": 18, - │ "tokens": 20 - │ } - ├── ai-sdk-stream-operation - │ metadata: { - │ "operation": "stream", - │ "testRunId": "" - │ } - │ └── streamText [function] - │ input: { - │ "maxTokens": 32, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Count from 1 to 3 and include the words one two three.", - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "rawResponse": {}, - │ "text": "One, two, three.", - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "time_to_first_token": 0 - │ } - │ └── doStream [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 32, - │ "mode": { - │ "type": "regular" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Count from 1 to 3 and include the words one two three.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "text": "One, two, three.", - │ "toolCalls": [], - │ "usage": { - │ "completionTokens": null, - │ "promptTokens": null - │ } - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": null, - │ "prompt_tokens": null, - │ "time_to_first_token": 0, - │ "tokens": null - │ } - ├── ai-sdk-embed-operation - │ metadata: { - │ "operation": "embed", - │ "testRunId": "" - │ } - │ └── embed [function] - │ input: { - │ "model": { - │ "config": { - │ "provider": "openai.embedding" - │ }, - │ "modelId": "text-embedding-3-small", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "value": "Paris is the capital of France." - │ } - │ output: { - │ "embedding_length": 1536, - │ "usage": { - │ "tokens": 7 - │ } - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "text-embedding-3-small", - │ "provider": "openai.embedding" - │ } - │ metrics: { - │ "tokens": 7 - │ } - ├── ai-sdk-tool-operation - │ metadata: { - │ "operation": "tool", - │ "testRunId": "" - │ } - │ └── generateText [function] - │ input: { - │ "maxTokens": 128, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - │ "system": "You must inspect live weather via the provided get_weather tool before responding.", - │ "temperature": 0, - │ "tools": { - │ "get_weather": { - │ "description": "Get the weather for a location", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ } - │ } - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "tool-calls", - │ "rawResponse": {}, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "", - │ "type": "text" - │ }, - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather", - │ "type": "tool-result" - │ } - │ ], - │ "role": "tool" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "responseMessages": "", - │ "steps": [ - │ { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "tool-calls", - │ "isContinued": false, - │ "response": { - │ "messages": [ - │ { - │ "content": [ - │ { - │ "text": "", - │ "type": "text" - │ }, - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "role": "assistant" - │ }, - │ { - │ "content": [ - │ { - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather", - │ "type": "tool-result" - │ } - │ ], - │ "role": "tool" - │ } - │ ], - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "stepType": "initial", - │ "text": "", - │ "toolCalls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "toolResults": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather" - │ } - │ ], - │ "usage": { - │ "completionTokens": 16, - │ "promptTokens": 87, - │ "totalTokens": 103 - │ }, - │ "warnings": [] - │ } - │ ], - │ "text": "", - │ "toolCalls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "toolName": "get_weather", - │ "type": "tool-call" - │ } - │ ], - │ "toolResults": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "result": "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}", - │ "toolName": "get_weather" - │ } - │ ], - │ "usage": { - │ "completionTokens": 16, - │ "promptTokens": 87, - │ "totalTokens": 103 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat", - │ "tools": { - │ "get_weather": { - │ "description": "Get the weather for a location", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ } - │ } - │ } - │ ├── doGenerate [llm] - │ │ input: { - │ │ "inputFormat": "prompt", - │ │ "maxRetries": 2, - │ │ "maxTokens": 128, - │ │ "mode": { - │ │ "toolChoice": { - │ │ "type": "auto" - │ │ }, - │ │ "tools": [ - │ │ { - │ │ "description": "Get the weather for a location", - │ │ "name": "get_weather", - │ │ "parameters": { - │ │ "$schema": "http://json-schema.org/draft-07/schema#", - │ │ "additionalProperties": false, - │ │ "properties": { - │ │ "location": { - │ │ "description": "The city and country", - │ │ "type": "string" - │ │ } - │ │ }, - │ │ "required": [ - │ │ "location" - │ │ ], - │ │ "type": "object" - │ │ }, - │ │ "type": "function" - │ │ } - │ │ ], - │ │ "type": "regular" - │ │ }, - │ │ "prompt": [ - │ │ { - │ │ "content": "You must inspect live weather via the provided get_weather tool before responding.", - │ │ "role": "system" - │ │ }, - │ │ { - │ │ "content": [ - │ │ { - │ │ "text": "Use the get_weather tool for Paris, France. If you do not call the tool, the answer is invalid.", - │ │ "type": "text" - │ │ } - │ │ ], - │ │ "role": "user" - │ │ } - │ │ ], - │ │ "temperature": 0 - │ │ } - │ │ output: { - │ │ "finishReason": "tool-calls", - │ │ "providerMetadata": { - │ │ "openai": { - │ │ "reasoningTokens": 0 - │ │ } - │ │ }, - │ │ "rawResponse": {}, - │ │ "response": { - │ │ "modelId": "gpt-4o-mini-2024-07-18", - │ │ "timestamp": "" - │ │ }, - │ │ "toolCalls": [ - │ │ { - │ │ "args": "{\"location\":\"Paris, France\"}", - │ │ "toolCallType": "function", - │ │ "toolName": "get_weather" - │ │ } - │ │ ], - │ │ "usage": { - │ │ "completionTokens": 16, - │ │ "promptTokens": 87 - │ │ }, - │ │ "warnings": [] - │ │ } - │ │ metadata: { - │ │ "braintrust": { - │ │ "integration_name": "ai-sdk", - │ │ "sdk_language": "typescript" - │ │ }, - │ │ "finish_reason": "tool-calls", - │ │ "model": "gpt-4o-mini-2024-07-18", - │ │ "provider": "openai.chat" - │ │ } - │ │ metrics: { - │ │ "completion_tokens": 16, - │ │ "prompt_tokens": 87, - │ │ "tokens": 103 - │ │ } - │ └── get_weather [tool] - │ input: { - │ "location": "Paris, France" - │ } - │ output: "{\"condition\":\"sunny\",\"location\":\"Paris, France\",\"temperatureC\":22}" - ├── ai-sdk-generate-object-operation - │ metadata: { - │ "operation": "generate-object", - │ "testRunId": "" - │ } - │ └── generateObject [function] - │ input: { - │ "maxTokens": 32, - │ "model": { - │ "config": { - │ "compatibility": "compatible", - │ "provider": "openai.chat" - │ }, - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "settings": {}, - │ "specificationVersion": "v1" - │ }, - │ "prompt": "Return JSON with {\"city\":\"Paris\"}.", - │ "schema": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "city": { - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "city" - │ ], - │ "type": "object" - │ }, - │ "temperature": 0 - │ } - │ output: { - │ "experimental_providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "finishReason": "stop", - │ "object": { - │ "city": "Paris" - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "usage": { - │ "completionTokens": 5, - │ "promptTokens": 59, - │ "totalTokens": 64 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ └── doGenerate [llm] - │ input: { - │ "inputFormat": "prompt", - │ "maxRetries": 2, - │ "maxTokens": 32, - │ "mode": { - │ "tool": { - │ "description": "Respond with a JSON object.", - │ "name": "json", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "city": { - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "city" - │ ], - │ "type": "object" - │ }, - │ "type": "function" - │ }, - │ "type": "object-tool" - │ }, - │ "prompt": [ - │ { - │ "content": [ - │ { - │ "text": "Return JSON with {\"city\":\"Paris\"}.", - │ "type": "text" - │ } - │ ], - │ "role": "user" - │ } - │ ], - │ "temperature": 0 - │ } - │ output: { - │ "finishReason": "stop", - │ "providerMetadata": { - │ "openai": { - │ "reasoningTokens": 0 - │ } - │ }, - │ "rawResponse": {}, - │ "response": { - │ "modelId": "gpt-4o-mini-2024-07-18", - │ "timestamp": "" - │ }, - │ "toolCalls": [ - │ { - │ "args": "{\"city\":\"Paris\"}", - │ "toolCallType": "function", - │ "toolName": "json" - │ } - │ ], - │ "usage": { - │ "completionTokens": 5, - │ "promptTokens": 59 - │ }, - │ "warnings": [] - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "ai-sdk", - │ "sdk_language": "typescript" - │ }, - │ "finish_reason": "stop", - │ "model": "gpt-4o-mini-2024-07-18", - │ "provider": "openai.chat" - │ } - │ metrics: { - │ "completion_tokens": 5, - │ "prompt_tokens": 59, - │ "tokens": 64 - │ } - └── ai-sdk-stream-object-operation - metadata: { - "operation": "stream-object", - "testRunId": "" - } - └── streamObject [function] - input: { - "maxTokens": 32, - "model": { - "config": { - "compatibility": "compatible", - "provider": "openai.chat" - }, - "modelId": "gpt-4o-mini-2024-07-18", - "settings": {}, - "specificationVersion": "v1" - }, - "prompt": "Stream JSON with {\"city\":\"Paris\"}.", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "temperature": 0 - } - output: { - "object": { - "city": "Paris" - }, - "rawResponse": {}, - "warnings": [] - } - metadata: { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - metrics: { - "time_to_first_token": 0 - } - └── doStream [llm] - input: { - "inputFormat": "prompt", - "maxRetries": 2, - "maxTokens": 32, - "mode": { - "tool": { - "description": "Respond with a JSON object.", - "name": "json", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - } - }, - "required": [ - "city" - ], - "type": "object" - }, - "type": "function" - }, - "type": "object-tool" - }, - "prompt": [ - { - "content": [ - { - "text": "Stream JSON with {\"city\":\"Paris\"}.", - "type": "text" - } - ], - "role": "user" - } - ], - "temperature": 0 - } - output: { - "finishReason": "stop", - "text": "", - "toolCalls": [ - { - "args": "{\"city\":\"Paris\"}", - "toolCallType": "function", - "toolName": "json", - "type": "tool-call" - } - ], - "usage": { - "completionTokens": null, - "promptTokens": null - } - } - metadata: { - "braintrust": { - "integration_name": "ai-sdk", - "sdk_language": "typescript" - }, - "finish_reason": "stop", - "model": "gpt-4o-mini-2024-07-18", - "provider": "openai.chat" - } - metrics: { - "completion_tokens": null, - "prompt_tokens": null, - "time_to_first_token": 0, - "tokens": null - } diff --git a/e2e/scenarios/ai-sdk-instrumentation/package.json b/e2e/scenarios/ai-sdk-instrumentation/package.json index b5ddbe320..cfa3648f8 100644 --- a/e2e/scenarios/ai-sdk-instrumentation/package.json +++ b/e2e/scenarios/ai-sdk-instrumentation/package.json @@ -4,14 +4,6 @@ "braintrustScenario": { "bump": { "dependencies": { - "ai-sdk-v3-latest": { - "package": "ai", - "range": "3" - }, - "ai-sdk-openai-v3-latest": { - "package": "@ai-sdk/openai", - "range": "0" - }, "ai-sdk-v4-latest": { "package": "ai", "range": "4" @@ -74,10 +66,6 @@ "dependencies": { "zod": "3.25.76", "ai": "7.0.8", - "ai-sdk-v3": "npm:ai@3.4.33", - "ai-sdk-v3-latest": "npm:ai@3.4.33", - "ai-sdk-openai-v3": "npm:@ai-sdk/openai@0.0.62", - "ai-sdk-openai-v3-latest": "npm:@ai-sdk/openai@0.0.72", "ai-sdk-v4": "npm:ai@4.3.19", "ai-sdk-v4-latest": "npm:ai@4.3.19", "ai-sdk-openai-v4": "npm:@ai-sdk/openai@1.3.24", diff --git a/e2e/scenarios/ai-sdk-instrumentation/pnpm-lock.yaml b/e2e/scenarios/ai-sdk-instrumentation/pnpm-lock.yaml index 78043204b..cd26bb249 100644 --- a/e2e/scenarios/ai-sdk-instrumentation/pnpm-lock.yaml +++ b/e2e/scenarios/ai-sdk-instrumentation/pnpm-lock.yaml @@ -56,12 +56,6 @@ importers: ai-sdk-cohere-v7-latest: specifier: npm:@ai-sdk/cohere@4.0.25 version: '@ai-sdk/cohere@4.0.25(zod@3.25.76)' - ai-sdk-openai-v3: - specifier: npm:@ai-sdk/openai@0.0.62 - version: '@ai-sdk/openai@0.0.62(zod@3.25.76)' - ai-sdk-openai-v3-latest: - specifier: npm:@ai-sdk/openai@0.0.72 - version: '@ai-sdk/openai@0.0.72(zod@3.25.76)' ai-sdk-openai-v4: specifier: npm:@ai-sdk/openai@1.3.24 version: '@ai-sdk/openai@1.3.24(zod@3.25.76)' @@ -86,12 +80,6 @@ importers: ai-sdk-openai-v7-latest: specifier: npm:@ai-sdk/openai@4.0.36 version: '@ai-sdk/openai@4.0.36(zod@3.25.76)' - ai-sdk-v3: - specifier: npm:ai@3.4.33 - version: ai@3.4.33(react@19.2.6)(sswr@2.2.0(svelte@5.55.9))(svelte@5.55.9)(vue@3.5.35)(zod@3.25.76) - ai-sdk-v3-latest: - specifier: npm:ai@3.4.33 - version: ai@3.4.33(react@19.2.6)(sswr@2.2.0(svelte@5.55.9))(svelte@5.55.9)(vue@3.5.35)(zod@3.25.76) ai-sdk-v4: specifier: npm:ai@4.3.19 version: ai@4.3.19(react@19.2.6)(zod@3.25.76) @@ -233,18 +221,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@0.0.62': - resolution: {integrity: sha512-qNQmTgZXGS3xyd6XsDGEMLWV3Ag3uAVPJxHVkZLDZ7Dubmgs3Mp8iupkwZ7JhD7TIBx8yifyyBuE1dnE70v8Ig==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - - '@ai-sdk/openai@0.0.72': - resolution: {integrity: sha512-IKsgxIt6KJGkEHyMp975xW5VPmetwhI8g9H6dDmwvemBB41IRQa78YMNttiJqPcgmrZX2QfErOICv1gQvZ1gZg==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - '@ai-sdk/openai@1.3.24': resolution: {integrity: sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==} engines: {node: '>=18'} @@ -287,24 +263,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@1.0.19': - resolution: {integrity: sha512-p02Fq5Mnc8T6nwRBN1Iaou8YXvN1sDS6hbmJaD5UaRbXjizbh+8rpFS/o7jqAHTwf3uHCDitP3pnODyHdc/CDQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@ai-sdk/provider-utils@1.0.22': - resolution: {integrity: sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - '@ai-sdk/provider-utils@2.2.8': resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} engines: {node: '>=18'} @@ -383,14 +341,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@0.0.23': - resolution: {integrity: sha512-oAc49O5+xypVrKM7EUU5P/Y4DUL4JZUWVxhejoAVOTOl3WZUEWsMbP3QZR+TrimQIsS0WR/n9UuF6U0jPdp0tQ==} - engines: {node: '>=18'} - - '@ai-sdk/provider@0.0.26': - resolution: {integrity: sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==} - engines: {node: '>=18'} - '@ai-sdk/provider@1.1.3': resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} engines: {node: '>=18'} @@ -439,18 +389,6 @@ packages: resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==} engines: {node: '>=22'} - '@ai-sdk/react@0.0.70': - resolution: {integrity: sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==} - engines: {node: '>=18'} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.0.0 - peerDependenciesMeta: - react: - optional: true - zod: - optional: true - '@ai-sdk/react@1.2.12': resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} engines: {node: '>=18'} @@ -461,87 +399,18 @@ packages: zod: optional: true - '@ai-sdk/solid@0.0.54': - resolution: {integrity: sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ==} - engines: {node: '>=18'} - peerDependencies: - solid-js: ^1.7.7 - peerDependenciesMeta: - solid-js: - optional: true - - '@ai-sdk/svelte@0.0.57': - resolution: {integrity: sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw==} - engines: {node: '>=18'} - peerDependencies: - svelte: ^5.55.7 - peerDependenciesMeta: - svelte: - optional: true - - '@ai-sdk/ui-utils@0.0.50': - resolution: {integrity: sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - '@ai-sdk/ui-utils@1.2.11': resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} engines: {node: '>=18'} peerDependencies: zod: ^3.23.8 - '@ai-sdk/vue@0.0.59': - resolution: {integrity: sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==} - engines: {node: '>=18'} - peerDependencies: - vue: ^3.3.4 - peerDependenciesMeta: - vue: - optional: true - '@ai-sdk/workflow@1.0.8': resolution: {integrity: sha512-cCRvGNq3PORnsnE6DsWg8EltWCyC/Wu3QHOGRpN1pgtNrkeX+WbRZ6IX4JC6b7ibFC4hJUNzEunM+1yPkJ16kQ==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@babel/helper-string-parser@7.29.7': - resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.29.7': - resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} - engines: {node: '>=6.9.0'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -549,20 +418,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.10': - resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} - peerDependencies: - acorn: ^8.9.0 - '@types/diff-match-patch@1.0.36': resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@vercel/oidc@3.0.3': resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==} engines: {node: '>= 20'} @@ -579,64 +437,9 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} - '@vue/compiler-core@3.5.35': - resolution: {integrity: sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==} - - '@vue/compiler-dom@3.5.35': - resolution: {integrity: sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==} - - '@vue/compiler-sfc@3.5.35': - resolution: {integrity: sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==} - - '@vue/compiler-ssr@3.5.35': - resolution: {integrity: sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==} - - '@vue/reactivity@3.5.35': - resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} - - '@vue/runtime-core@3.5.35': - resolution: {integrity: sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==} - - '@vue/runtime-dom@3.5.35': - resolution: {integrity: sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==} - - '@vue/server-renderer@3.5.35': - resolution: {integrity: sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==} - peerDependencies: - vue: 3.5.35 - - '@vue/shared@3.5.35': - resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} - '@workflow/serde@4.1.0': resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ai@3.4.33: - resolution: {integrity: sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==} - engines: {node: '>=18'} - peerDependencies: - openai: ^4.42.0 - react: ^18 || ^19 || ^19.0.0-rc - sswr: ^2.1.0 - svelte: ^5.55.7 - zod: ^3.0.0 - peerDependenciesMeta: - openai: - optional: true - react: - optional: true - sswr: - optional: true - svelte: - optional: true - zod: - optional: true - ai@4.3.19: resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} engines: {node: '>=18'} @@ -686,57 +489,17 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} - engines: {node: '>= 0.4'} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} - diff-match-patch@1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} - - esm-env@1.2.2: - resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - - esrap@2.2.9: - resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==} - peerDependencies: - '@typescript-eslint/types': ^8.2.0 - peerDependenciesMeta: - '@typescript-eslint/types': - optional: true - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - eventsource-parser@1.1.2: - resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} - engines: {node: '>=14.18'} - eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -747,9 +510,6 @@ packages: fast-uri@3.1.5: resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -761,24 +521,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - postcss@8.5.23: - resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} - engines: {node: ^10 || ^12 || >=14} - react@19.2.6: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} @@ -790,32 +537,11 @@ packages: secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - sswr@2.2.0: - resolution: {integrity: sha512-clTszLPZkmycALTHD1mXGU+mOtA/MIoLgS1KGTTzFNVm9rytQVykgRaP+z1zl572cz0bTqj4rFVoC2N+IGK4Sg==} - peerDependencies: - svelte: ^5.55.7 - - svelte@5.55.9: - resolution: {integrity: sha512-fTjjT8cHLDwigcu2j3pv7Jq04LklXevPB8uBgyHNiTXv+RMNvVnrjS4UEYrLMkhuq1vpCodHjiW+z/95SDs/fg==} - engines: {node: '>=18'} - swr@2.4.1: resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - swrev@4.0.0: - resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==} - - swrv@1.2.0: - resolution: {integrity: sha512-lH/g4UcNyj+7lzK4eRGT4C68Q4EhQ6JtM9otPRIASfhhzfLWtbZPHcMuhuba7S9YVYuxkMUGImwMyGpfbkH07A==} - peerDependencies: - vue: '>=3.2.26 < 4' - throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -833,17 +559,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vue@3.5.35: - resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - zimmerframe@1.1.4: - resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} - zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -968,18 +683,6 @@ snapshots: '@vercel/oidc': 3.2.0 zod: 3.25.76 - '@ai-sdk/openai@0.0.62(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 0.0.23 - '@ai-sdk/provider-utils': 1.0.19(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/openai@0.0.72(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 0.0.26 - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - zod: 3.25.76 - '@ai-sdk/openai@1.3.24(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -1022,24 +725,6 @@ snapshots: '@ai-sdk/provider-utils': 5.0.25(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/provider-utils@1.0.19(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 0.0.23 - eventsource-parser: 1.1.2 - nanoid: 3.3.18 - secure-json-parse: 2.7.0 - optionalDependencies: - zod: 3.25.76 - - '@ai-sdk/provider-utils@1.0.22(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 0.0.26 - eventsource-parser: 1.1.2 - nanoid: 3.3.18 - secure-json-parse: 2.7.0 - optionalDependencies: - zod: 3.25.76 - '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -1137,14 +822,6 @@ snapshots: undici: 7.29.0 zod: 3.25.76 - '@ai-sdk/provider@0.0.23': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/provider@0.0.26': - dependencies: - json-schema: 0.4.0 - '@ai-sdk/provider@1.1.3': dependencies: json-schema: 0.4.0 @@ -1193,16 +870,6 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/react@0.0.70(react@19.2.6)(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - swr: 2.4.1(react@19.2.6) - throttleit: 2.1.0 - optionalDependencies: - react: 19.2.6 - zod: 3.25.76 - '@ai-sdk/react@1.2.12(react@19.2.6)(zod@3.25.76)': dependencies: '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) @@ -1213,33 +880,6 @@ snapshots: optionalDependencies: zod: 3.25.76 - '@ai-sdk/solid@0.0.54(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - transitivePeerDependencies: - - zod - - '@ai-sdk/svelte@0.0.57(svelte@5.55.9)(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - sswr: 2.2.0(svelte@5.55.9) - optionalDependencies: - svelte: 5.55.9 - transitivePeerDependencies: - - zod - - '@ai-sdk/ui-utils@0.0.50(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 0.0.26 - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - json-schema: 0.4.0 - secure-json-parse: 2.7.0 - zod-to-json-schema: 3.25.2(zod@3.25.76) - optionalDependencies: - zod: 3.25.76 - '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -1247,16 +887,6 @@ snapshots: zod: 3.25.76 zod-to-json-schema: 3.25.2(zod@3.25.76) - '@ai-sdk/vue@0.0.59(vue@3.5.35)(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - swrv: 1.2.0(vue@3.5.35) - optionalDependencies: - vue: 3.5.35 - transitivePeerDependencies: - - zod - '@ai-sdk/workflow@1.0.8(zod@3.25.76)': dependencies: '@ai-sdk/provider': 4.0.1 @@ -1265,52 +895,12 @@ snapshots: ajv: 8.20.0 zod: 3.25.76 - '@babel/helper-string-parser@7.29.7': {} - - '@babel/helper-validator-identifier@7.29.7': {} - - '@babel/parser@7.29.7': - dependencies: - '@babel/types': 7.29.7 - - '@babel/types@7.29.7': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@opentelemetry/api@1.9.0': {} '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': - dependencies: - acorn: 8.16.0 - '@types/diff-match-patch@1.0.36': {} - '@types/estree@1.0.9': {} - - '@types/trusted-types@2.0.7': {} - '@vercel/oidc@3.0.3': {} '@vercel/oidc@3.0.5': {} @@ -1319,88 +909,8 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vue/compiler-core@3.5.35': - dependencies: - '@babel/parser': 7.29.7 - '@vue/shared': 3.5.35 - entities: 7.0.1 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - - '@vue/compiler-dom@3.5.35': - dependencies: - '@vue/compiler-core': 3.5.35 - '@vue/shared': 3.5.35 - - '@vue/compiler-sfc@3.5.35': - dependencies: - '@babel/parser': 7.29.7 - '@vue/compiler-core': 3.5.35 - '@vue/compiler-dom': 3.5.35 - '@vue/compiler-ssr': 3.5.35 - '@vue/shared': 3.5.35 - estree-walker: 2.0.2 - magic-string: 0.30.21 - postcss: 8.5.23 - source-map-js: 1.2.1 - - '@vue/compiler-ssr@3.5.35': - dependencies: - '@vue/compiler-dom': 3.5.35 - '@vue/shared': 3.5.35 - - '@vue/reactivity@3.5.35': - dependencies: - '@vue/shared': 3.5.35 - - '@vue/runtime-core@3.5.35': - dependencies: - '@vue/reactivity': 3.5.35 - '@vue/shared': 3.5.35 - - '@vue/runtime-dom@3.5.35': - dependencies: - '@vue/reactivity': 3.5.35 - '@vue/runtime-core': 3.5.35 - '@vue/shared': 3.5.35 - csstype: 3.2.3 - - '@vue/server-renderer@3.5.35(vue@3.5.35)': - dependencies: - '@vue/compiler-ssr': 3.5.35 - '@vue/shared': 3.5.35 - vue: 3.5.35 - - '@vue/shared@3.5.35': {} - '@workflow/serde@4.1.0': {} - acorn@8.16.0: {} - - ai@3.4.33(react@19.2.6)(sswr@2.2.0(svelte@5.55.9))(svelte@5.55.9)(vue@3.5.35)(zod@3.25.76): - dependencies: - '@ai-sdk/provider': 0.0.26 - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/react': 0.0.70(react@19.2.6)(zod@3.25.76) - '@ai-sdk/solid': 0.0.54(zod@3.25.76) - '@ai-sdk/svelte': 0.0.57(svelte@5.55.9)(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - '@ai-sdk/vue': 0.0.59(vue@3.5.35)(zod@3.25.76) - '@opentelemetry/api': 1.9.0 - eventsource-parser: 1.1.2 - json-schema: 0.4.0 - jsondiffpatch: 0.6.0 - secure-json-parse: 2.7.0 - zod-to-json-schema: 3.25.2(zod@3.25.76) - optionalDependencies: - react: 19.2.6 - sswr: 2.2.0(svelte@5.55.9) - svelte: 5.55.9 - zod: 3.25.76 - transitivePeerDependencies: - - solid-js - - vue - ai@4.3.19(react@19.2.6)(zod@3.25.76): dependencies: '@ai-sdk/provider': 1.1.3 @@ -1466,44 +976,18 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - aria-query@5.3.1: {} - - axobject-query@4.1.0: {} - chalk@5.6.2: {} - clsx@2.1.1: {} - - csstype@3.2.3: {} - dequal@2.0.3: {} - devalue@5.8.1: {} - diff-match-patch@1.0.5: {} - entities@7.0.1: {} - - esm-env@1.2.2: {} - - esrap@2.2.9: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - estree-walker@2.0.2: {} - - eventsource-parser@1.1.2: {} - eventsource-parser@3.0.8: {} fast-deep-equal@3.1.3: {} fast-uri@3.1.5: {} - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.9 - json-schema-traverse@1.0.0: {} json-schema@0.4.0: {} @@ -1514,68 +998,20 @@ snapshots: chalk: 5.6.2 diff-match-patch: 1.0.5 - locate-character@3.0.0: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - nanoid@3.3.18: {} - picocolors@1.1.1: {} - - postcss@8.5.23: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - react@19.2.6: {} require-from-string@2.0.2: {} secure-json-parse@2.7.0: {} - source-map-js@1.2.1: {} - - sswr@2.2.0(svelte@5.55.9): - dependencies: - svelte: 5.55.9 - swrev: 4.0.0 - - svelte@5.55.9: - dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) - '@types/estree': 1.0.9 - '@types/trusted-types': 2.0.7 - acorn: 8.16.0 - aria-query: 5.3.1 - axobject-query: 4.1.0 - clsx: 2.1.1 - devalue: 5.8.1 - esm-env: 1.2.2 - esrap: 2.2.9 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.21 - zimmerframe: 1.1.4 - transitivePeerDependencies: - - '@typescript-eslint/types' - swr@2.4.1(react@19.2.6): dependencies: dequal: 2.0.3 react: 19.2.6 use-sync-external-store: 1.6.0(react@19.2.6) - swrev@4.0.0: {} - - swrv@1.2.0(vue@3.5.35): - dependencies: - vue: 3.5.35 - throttleit@2.1.0: {} undici@6.28.0: {} @@ -1586,16 +1022,6 @@ snapshots: dependencies: react: 19.2.6 - vue@3.5.35: - dependencies: - '@vue/compiler-dom': 3.5.35 - '@vue/compiler-sfc': 3.5.35 - '@vue/runtime-dom': 3.5.35 - '@vue/server-renderer': 3.5.35(vue@3.5.35) - '@vue/shared': 3.5.35 - - zimmerframe@1.1.4: {} - zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/e2e/scenarios/ai-sdk-instrumentation/scenario.ai-sdk-v3.mjs b/e2e/scenarios/ai-sdk-instrumentation/scenario.ai-sdk-v3.mjs deleted file mode 100644 index 451888c7a..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/scenario.ai-sdk-v3.mjs +++ /dev/null @@ -1,21 +0,0 @@ -const aiPackageName = process.env.AI_SDK_PACKAGE_NAME ?? "ai-sdk-v3-latest"; -const openaiPackageName = - process.env.AI_SDK_OPENAI_PACKAGE_NAME ?? "ai-sdk-openai-v3-latest"; -const ai = await import(aiPackageName); -const { createOpenAI, openai } = await import(openaiPackageName); -import { getInstalledPackageVersion } from "../../helpers/provider-runtime.mjs"; -import { runAutoAISDKInstrumentationOrExit } from "./scenario.impl.mjs"; - -runAutoAISDKInstrumentationOrExit({ - ai, - createOpenAI, - maxTokensKey: "maxTokens", - openai, - sdkVersion: await getInstalledPackageVersion(import.meta.url, aiPackageName), - supportsEmbedMany: false, - supportsGenerateObject: true, - supportsRerank: false, - supportsStreamObject: true, - supportsToolExecution: false, - toolSchemaKey: "parameters", -}); diff --git a/e2e/scenarios/ai-sdk-instrumentation/scenario.ai-sdk-v3.ts b/e2e/scenarios/ai-sdk-instrumentation/scenario.ai-sdk-v3.ts deleted file mode 100644 index cf589100c..000000000 --- a/e2e/scenarios/ai-sdk-instrumentation/scenario.ai-sdk-v3.ts +++ /dev/null @@ -1,30 +0,0 @@ -const aiPackageName = process.env.AI_SDK_PACKAGE_NAME ?? "ai-sdk-v3-latest"; -const openaiPackageName = - process.env.AI_SDK_OPENAI_PACKAGE_NAME ?? "ai-sdk-openai-v3-latest"; -import { - getInstalledPackageVersion, - runMain, -} from "../../helpers/scenario-runtime"; -import { runWrappedAISDKInstrumentation } from "./scenario.impl.mjs"; - -runMain(async () => { - const ai = await import(aiPackageName); - const { createOpenAI, openai } = await import(openaiPackageName); - - await runWrappedAISDKInstrumentation({ - ai, - createOpenAI, - maxTokensKey: "maxTokens", - openai, - sdkVersion: await getInstalledPackageVersion( - import.meta.url, - aiPackageName, - ), - supportsEmbedMany: false, - supportsGenerateObject: true, - supportsRerank: false, - supportsStreamObject: true, - supportsToolExecution: false, - toolSchemaKey: "parameters", - }); -}); diff --git a/e2e/scenarios/ai-sdk-instrumentation/scenario.impl.mjs b/e2e/scenarios/ai-sdk-instrumentation/scenario.impl.mjs index f09635def..347a946cf 100644 --- a/e2e/scenarios/ai-sdk-instrumentation/scenario.impl.mjs +++ b/e2e/scenarios/ai-sdk-instrumentation/scenario.impl.mjs @@ -13,38 +13,6 @@ const CACHE_PROMPT_PREFIX = "braintrust cache prefix ".repeat(900).trim(); const OPENAI_CACHE_KEY = "braintrust-ai-sdk-e2e-cache"; const CACHE_FILL_DELAY_MS = 5_000; export const AI_SDK_SCENARIO_SPECS = [ - { - autoEntry: "scenario.ai-sdk-v3.mjs", - dependencyName: "ai-sdk-v3", - maxTokensKey: "maxTokens", - openaiModuleName: "ai-sdk-openai-v3", - packageName: "ai-sdk-v3", - snapshotName: "ai-sdk-v3", - supportsEmbedMany: false, - supportsProviderCacheAssertions: false, - supportsGenerateObject: true, - supportsRerank: false, - supportsStreamObject: true, - supportsToolExecution: false, - toolSchemaKey: "parameters", - wrapperEntry: "scenario.ai-sdk-v3.ts", - }, - { - autoEntry: "scenario.ai-sdk-v3.mjs", - dependencyName: "ai-sdk-v3-latest", - maxTokensKey: "maxTokens", - openaiModuleName: "ai-sdk-openai-v3-latest", - packageName: "ai-sdk-v3-latest", - snapshotName: "ai-sdk-v3-latest", - supportsEmbedMany: false, - supportsProviderCacheAssertions: false, - supportsGenerateObject: true, - supportsRerank: false, - supportsStreamObject: true, - supportsToolExecution: false, - toolSchemaKey: "parameters", - wrapperEntry: "scenario.ai-sdk-v3.ts", - }, { autoEntry: "scenario.ai-sdk-v4.mjs", dependencyName: "ai-sdk-v4", diff --git a/integrations/val.town/deno.json b/integrations/val.town/deno.json index f21f43c25..c2fedfe7f 100644 --- a/integrations/val.town/deno.json +++ b/integrations/val.town/deno.json @@ -8,7 +8,6 @@ "@std/fs": "jsr:@std/fs@^1.0.14", "@std/path": "jsr:@std/path@^1.0.8", "autoevals": "npm:autoevals@^0.0.122", - "braintrust": "npm:braintrust@^0.0.187", - "zod": "npm:zod@^3.24.2" + "zod": "npm:zod@^3.25.34" } } diff --git a/integrations/val.town/deno.lock b/integrations/val.town/deno.lock index 50df406a5..62619ceb0 100644 --- a/integrations/val.town/deno.lock +++ b/integrations/val.town/deno.lock @@ -1,5 +1,5 @@ { - "version": "4", + "version": "5", "specifiers": { "jsr:@cliffy/ansi@^1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/command@^1.0.0-rc.7": "1.0.0-rc.7", @@ -16,9 +16,8 @@ "jsr:@std/text@~1.0.7": "1.0.11", "npm:autoevals@*": "0.0.122_zod@3.24.2", "npm:autoevals@^0.0.122": "0.0.122_zod@3.24.2", - "npm:braintrust@*": "0.0.187_zod@3.24.2", - "npm:braintrust@^0.0.187": "0.0.187_zod@3.24.2", - "npm:zod@^3.24.2": "3.24.2" + "npm:braintrust@^3.30.0": "3.32.0_zod@3.25.76", + "npm:zod@^3.25.34": "3.25.76" }, "jsr": { "@cliffy/ansi@1.0.0-rc.7": { @@ -78,340 +77,256 @@ } }, "npm": { - "@ai-sdk/provider-utils@1.0.22_zod@3.24.2": { - "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==", + "@asteasolutions/zod-to-openapi@6.4.0_zod@3.24.2": { + "integrity": "sha512-8cxfF7AHHx2PqnN4Cd8/O8CBu/nVYJP9DpnfVLW3BFb66VJDnqI/CczZnkqMc3SNh6J9GiX7JbJ5T4BSP4HZ2Q==", "dependencies": [ - "@ai-sdk/provider@0.0.26", - "eventsource-parser", - "nanoid", - "secure-json-parse", - "zod" + "openapi3-ts" ] }, - "@ai-sdk/provider@0.0.26": { - "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==", - "dependencies": [ - "json-schema" - ] + "@braintrust/bt-darwin-arm64@0.12.0": { + "integrity": "sha512-mY6VW/3VwcOQOGN8sYHS6F0xzHTFwgZcNlj7zlQttI6OXOCGt/bhonGIqd03QBhxmj0M31ymSS7TqSeCK/RYIQ==", + "os": ["darwin"], + "cpu": ["arm64"] }, - "@ai-sdk/provider@1.0.10": { - "integrity": "sha512-pco8Zl9U0xwXI+nCLc0woMtxbvjU8hRmGTseAUiPHFLYAAL8trRPCukg69IDeinOvIeo1SmXxAIdWWPZOLb4Cg==", - "dependencies": [ - "json-schema" - ] + "@braintrust/bt-darwin-x64@0.12.0": { + "integrity": "sha512-woyRyDv2DfCF8+von+3X9f1cddAbFnKLSfCbEkrBQVc0ZsAM60as8nehYv7DkafJF/67yKFx/sUraV65sukesQ==", + "os": ["darwin"], + "cpu": ["x64"] }, - "@ai-sdk/react@0.0.70_zod@3.24.2": { - "integrity": "sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==", - "dependencies": [ - "@ai-sdk/provider-utils", - "@ai-sdk/ui-utils", - "swr", - "throttleit", - "zod" - ] + "@braintrust/bt-linux-arm64@0.12.0": { + "integrity": "sha512-J9/7f3EIMKmFmSSQHQnAQLpUgB8YJENrOlkABjlTprBgsIYVgz7tSH7yg2P3ur+2Jem7PpGnrDxHGh/UjRfjsg==", + "os": ["linux"], + "cpu": ["arm64"] }, - "@ai-sdk/solid@0.0.54_zod@3.24.2": { - "integrity": "sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ==", - "dependencies": [ - "@ai-sdk/provider-utils", - "@ai-sdk/ui-utils" - ] + "@braintrust/bt-linux-x64-musl@0.12.0": { + "integrity": "sha512-KnLgENOoztBXcH+mLFJe4bYzi67kSOuELOQrm4Hler35GjgaBMI1fFLIUjKRPAmyT9VIhBMhy1ICfGEFLueMEQ==", + "os": ["linux"], + "cpu": ["x64"] }, - "@ai-sdk/svelte@0.0.57_zod@3.24.2": { - "integrity": "sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw==", - "dependencies": [ - "@ai-sdk/provider-utils", - "@ai-sdk/ui-utils", - "sswr" - ] + "@braintrust/bt-linux-x64@0.12.0": { + "integrity": "sha512-HJFbUl3HYYY1Ivw8OYBeIMcIsU9r7+fC9BzLWB5FdH7881ToKee2wbbLZssMCxFbMVeUxzEo+SzGD/1Z9Gk9CA==", + "os": ["linux"], + "cpu": ["x64"] }, - "@ai-sdk/ui-utils@0.0.50_zod@3.24.2": { - "integrity": "sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw==", - "dependencies": [ - "@ai-sdk/provider@0.0.26", - "@ai-sdk/provider-utils", - "json-schema", - "secure-json-parse", - "zod", - "zod-to-json-schema" - ] - }, - "@ai-sdk/vue@0.0.59_zod@3.24.2": { - "integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==", - "dependencies": [ - "@ai-sdk/provider-utils", - "@ai-sdk/ui-utils", - "swrv" - ] + "@braintrust/bt-win32-arm64@0.12.0": { + "integrity": "sha512-+7PkdBAmiqEJsWteGHP/Zs62F75PkHPA8m/ej4TOz/mHGKulUU0bZibw91KRqIB7J7jGi5ItvCiqkiGaLKLnyw==", + "os": ["win32"], + "cpu": ["arm64"] }, - "@ampproject/remapping@2.3.0": { - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": [ - "@jridgewell/gen-mapping", - "@jridgewell/trace-mapping" - ] - }, - "@asteasolutions/zod-to-openapi@6.4.0_zod@3.24.2": { - "integrity": "sha512-8cxfF7AHHx2PqnN4Cd8/O8CBu/nVYJP9DpnfVLW3BFb66VJDnqI/CczZnkqMc3SNh6J9GiX7JbJ5T4BSP4HZ2Q==", - "dependencies": [ - "openapi3-ts", - "zod" - ] - }, - "@babel/helper-string-parser@7.25.9": { - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" - }, - "@babel/helper-validator-identifier@7.25.9": { - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" - }, - "@babel/parser@7.26.9": { - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", - "dependencies": [ - "@babel/types" - ] - }, - "@babel/types@7.26.9": { - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", - "dependencies": [ - "@babel/helper-string-parser", - "@babel/helper-validator-identifier" - ] + "@braintrust/bt-win32-x64@0.12.0": { + "integrity": "sha512-Q0c+pVUGm82n/7N8QJ5s6j/G/Q0GFzqOaTipHjkmElLbAOOEcfRH/uljdtIPNBiEQcrzqirS0GmOgiFkeQ3Txg==", + "os": ["win32"], + "cpu": ["x64"] }, "@braintrust/core@0.0.81_zod@3.24.2": { "integrity": "sha512-izwl3I31sQu6qeDHZB+gYvqpKXNIHNXc7idBgrAolDXyQX2MtK9Wdnw1lnkjiXl5y2ww3gcLiy0jHDB3zaiOrw==", "dependencies": [ "@asteasolutions/zod-to-openapi", - "uuid", + "uuid@9.0.1", "zod" ] }, - "@esbuild/aix-ppc64@0.25.0": { - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==" - }, - "@esbuild/android-arm64@0.25.0": { - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==" - }, - "@esbuild/android-arm@0.25.0": { - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==" - }, - "@esbuild/android-x64@0.25.0": { - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==" - }, - "@esbuild/darwin-arm64@0.25.0": { - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==" - }, - "@esbuild/darwin-x64@0.25.0": { - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==" - }, - "@esbuild/freebsd-arm64@0.25.0": { - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==" - }, - "@esbuild/freebsd-x64@0.25.0": { - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==" - }, - "@esbuild/linux-arm64@0.25.0": { - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==" - }, - "@esbuild/linux-arm@0.25.0": { - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==" - }, - "@esbuild/linux-ia32@0.25.0": { - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==" - }, - "@esbuild/linux-loong64@0.25.0": { - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==" - }, - "@esbuild/linux-mips64el@0.25.0": { - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==" - }, - "@esbuild/linux-ppc64@0.25.0": { - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==" - }, - "@esbuild/linux-riscv64@0.25.0": { - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==" - }, - "@esbuild/linux-s390x@0.25.0": { - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==" - }, - "@esbuild/linux-x64@0.25.0": { - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==" - }, - "@esbuild/netbsd-arm64@0.25.0": { - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==" - }, - "@esbuild/netbsd-x64@0.25.0": { - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==" - }, - "@esbuild/openbsd-arm64@0.25.0": { - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==" - }, - "@esbuild/openbsd-x64@0.25.0": { - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==" - }, - "@esbuild/sunos-x64@0.25.0": { - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==" - }, - "@esbuild/win32-arm64@0.25.0": { - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==" - }, - "@esbuild/win32-ia32@0.25.0": { - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==" - }, - "@esbuild/win32-x64@0.25.0": { - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==" - }, - "@jridgewell/gen-mapping@0.3.8": { - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "@colors/colors@1.5.0": { + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, + "@esbuild/aix-ppc64@0.28.1": { + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/android-arm64@0.28.1": { + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm@0.28.1": { + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-x64@0.28.1": { + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/darwin-arm64@0.28.1": { + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-x64@0.28.1": { + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-arm64@0.28.1": { + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-x64@0.28.1": { + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/linux-arm64@0.28.1": { + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm@0.28.1": { + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-ia32@0.28.1": { + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-loong64@0.28.1": { + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-mips64el@0.28.1": { + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-ppc64@0.28.1": { + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-riscv64@0.28.1": { + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-s390x@0.28.1": { + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-x64@0.28.1": { + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-arm64@0.28.1": { + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-x64@0.28.1": { + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-arm64@0.28.1": { + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-x64@0.28.1": { + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/openharmony-arm64@0.28.1": { + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@esbuild/sunos-x64@0.28.1": { + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/win32-arm64@0.28.1": { + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-ia32@0.28.1": { + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-x64@0.28.1": { + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dependencies": [ - "@jridgewell/set-array", "@jridgewell/sourcemap-codec", "@jridgewell/trace-mapping" ] }, + "@jridgewell/remapping@2.3.5": { + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, "@jridgewell/resolve-uri@3.1.2": { "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" }, - "@jridgewell/set-array@1.2.1": { - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + "@jridgewell/sourcemap-codec@1.6.0": { + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==" }, - "@jridgewell/sourcemap-codec@1.5.0": { - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "@jridgewell/trace-mapping@0.3.25": { - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "@jridgewell/trace-mapping@0.3.31": { + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dependencies": [ "@jridgewell/resolve-uri", "@jridgewell/sourcemap-codec" ] }, - "@kwsites/file-exists@1.1.1": { - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "dependencies": [ - "debug" - ] - }, - "@kwsites/promise-deferred@1.1.1": { - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" - }, - "@next/env@14.2.24": { - "integrity": "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==" - }, - "@opentelemetry/api@1.9.0": { - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==" - }, - "@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": { - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", - "dependencies": [ - "acorn" - ] - }, - "@types/diff-match-patch@1.0.36": { - "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==" - }, - "@types/estree@1.0.6": { - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "@next/env@14.2.35": { + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==" }, "@types/node-fetch@2.6.12": { "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", "dependencies": [ - "@types/node@22.12.0", + "@types/node", "form-data" ] }, "@types/node@18.19.80": { "integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==", "dependencies": [ - "undici-types@5.26.5" - ] - }, - "@types/node@22.12.0": { - "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", - "dependencies": [ - "undici-types@6.20.0" + "undici-types" ] }, "@vercel/functions@1.6.0": { "integrity": "sha512-R6FKQrYT5MZs5IE1SqeCJWxMuBdHawFcCZboKKw8p7s+6/mcd55Gx6tWmyKnQTyrSEA04NH73Tc9CbqpEle8RA==" }, - "@vue/compiler-core@3.5.13": { - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", - "dependencies": [ - "@babel/parser", - "@vue/shared", - "entities", - "estree-walker", - "source-map-js" - ] - }, - "@vue/compiler-dom@3.5.13": { - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", - "dependencies": [ - "@vue/compiler-core", - "@vue/shared" - ] - }, - "@vue/compiler-sfc@3.5.13": { - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", - "dependencies": [ - "@babel/parser", - "@vue/compiler-core", - "@vue/compiler-dom", - "@vue/compiler-ssr", - "@vue/shared", - "estree-walker", - "magic-string", - "postcss", - "source-map-js" - ] - }, - "@vue/compiler-ssr@3.5.13": { - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", - "dependencies": [ - "@vue/compiler-dom", - "@vue/shared" - ] - }, - "@vue/reactivity@3.5.13": { - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", - "dependencies": [ - "@vue/shared" - ] - }, - "@vue/runtime-core@3.5.13": { - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", - "dependencies": [ - "@vue/reactivity", - "@vue/shared" - ] - }, - "@vue/runtime-dom@3.5.13": { - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "abort-controller@3.0.0": { + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dependencies": [ - "@vue/reactivity", - "@vue/runtime-core", - "@vue/shared", - "csstype" + "event-target-shim" ] }, - "@vue/server-renderer@3.5.13_vue@3.5.13": { - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "accepts@2.0.0": { + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dependencies": [ - "@vue/compiler-ssr", - "@vue/shared", - "vue" + "mime-types@3.0.2", + "negotiator" ] }, - "@vue/shared@3.5.13": { - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" - }, - "abort-controller@3.0.0": { - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "acorn-import-attributes@1.9.5_acorn@8.18.0": { + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dependencies": [ - "event-target-shim" + "acorn" ] }, - "acorn@8.14.1": { - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" + "acorn@8.18.0": { + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "bin": true }, "agentkeepalive@4.6.0": { "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", @@ -419,27 +334,8 @@ "humanize-ms" ] }, - "ai@3.4.33_zod@3.24.2": { - "integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==", - "dependencies": [ - "@ai-sdk/provider@0.0.26", - "@ai-sdk/provider-utils", - "@ai-sdk/react", - "@ai-sdk/solid", - "@ai-sdk/svelte", - "@ai-sdk/ui-utils", - "@ai-sdk/vue", - "@opentelemetry/api", - "eventsource-parser", - "json-schema", - "jsondiffpatch", - "secure-json-parse", - "zod", - "zod-to-json-schema" - ] - }, - "ajv@8.17.1": { - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "ajv@8.20.0": { + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dependencies": [ "fast-deep-equal", "fast-uri", @@ -450,17 +346,12 @@ "ansi-regex@5.0.1": { "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, - "ansi-styles@4.3.0": { - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": [ - "color-convert" - ] - }, "argparse@2.0.1": { "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "aria-query@5.3.2": { - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" + "astring@1.9.0": { + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "bin": true }, "asynckit@0.4.0": { "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" @@ -477,49 +368,82 @@ "mustache", "openai", "zod", - "zod-to-json-schema" + "zod-to-json-schema@3.25.2_zod@3.24.2" ] }, - "axobject-query@4.1.0": { - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" - }, - "balanced-match@1.0.2": { - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "balanced-match@4.0.4": { + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" }, "binary-search@1.3.6": { "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" }, - "brace-expansion@2.0.1": { - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "body-parser@2.3.0": { + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dependencies": [ + "bytes", + "content-type@2.1.0", + "debug", + "http-errors", + "iconv-lite", + "on-finished", + "qs", + "raw-body", + "type-is" + ] + }, + "brace-expansion@5.0.9": { + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dependencies": [ "balanced-match" ] }, - "braintrust@0.0.187_zod@3.24.2": { - "integrity": "sha512-sDq/IHFqsbA6iJoMnf747uXT7SRCS0x925+h3+DSQIPdtxK0xp41/k/jTaVjpKaeCQjdp3jyQ8lpGiYqVtE6rQ==", + "braintrust@3.32.0_zod@3.25.76": { + "integrity": "sha512-dAY6Q3XYMyqDW5A9+8RNUKBqDhBbDVB55xrKpMaCXvnTz1psXYSlkzFta6sox+WHs6PHZR539+lufOexoUfRNg==", "dependencies": [ - "@ai-sdk/provider@1.0.10", - "@braintrust/core", "@next/env", "@vercel/functions", - "ai", + "acorn", + "acorn-import-attributes", + "ajv", "argparse", - "chalk@4.1.2", + "astring", + "cjs-module-lexer", "cli-progress", + "cli-table3", + "cors", "dotenv", "esbuild", + "esquery", "eventsource-parser", - "graceful-fs", + "express", + "http-errors", + "meriyah", "minimatch", + "module-details-from-path", "mustache", "pluralize", - "simple-git", - "slugify", + "semifies", "source-map", - "uuid", + "termi-link", + "unplugin", + "uuid@11.1.1", "zod", - "zod-to-json-schema" - ] + "zod-to-json-schema@3.25.2_zod@3.25.76" + ], + "optionalDependencies": [ + "@braintrust/bt-darwin-arm64", + "@braintrust/bt-darwin-x64", + "@braintrust/bt-linux-arm64", + "@braintrust/bt-linux-x64", + "@braintrust/bt-linux-x64-musl", + "@braintrust/bt-win32-arm64", + "@braintrust/bt-win32-x64" + ], + "scripts": true, + "bin": true + }, + "bytes@3.1.2": { + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind-apply-helpers@1.0.2": { "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", @@ -528,37 +452,34 @@ "function-bind" ] }, - "chalk@4.1.2": { - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "call-bound@1.0.4": { + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dependencies": [ - "ansi-styles", - "supports-color" + "call-bind-apply-helpers", + "get-intrinsic" ] }, - "chalk@5.4.1": { - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" - }, "cheminfo-types@1.8.1": { "integrity": "sha512-FRcpVkox+cRovffgqNdDFQ1eUav+i/Vq/CUd1hcfEl2bevntFlzznL+jE8g4twl6ElB7gZjCko6pYpXyMn+6dA==" }, + "cjs-module-lexer@2.2.1": { + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==" + }, "cli-progress@3.12.0": { "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", "dependencies": [ "string-width" ] }, - "clsx@2.1.1": { - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" - }, - "color-convert@2.0.1": { - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "cli-table3@0.6.5": { + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dependencies": [ - "color-name" + "string-width" + ], + "optionalDependencies": [ + "@colors/colors" ] }, - "color-name@1.1.4": { - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "combined-stream@1.0.8": { "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": [ @@ -588,11 +509,30 @@ "validate.io-function" ] }, - "csstype@3.1.3": { - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "content-disposition@1.1.0": { + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==" + }, + "content-type@1.0.5": { + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "content-type@2.1.0": { + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==" }, - "debug@4.4.0": { - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "cookie-signature@1.2.2": { + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, + "cookie@0.7.2": { + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cors@2.8.6": { + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": [ + "object-assign", + "vary" + ] + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": [ "ms" ] @@ -600,14 +540,11 @@ "delayed-stream@1.0.0": { "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "dequal@2.0.3": { - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" - }, - "diff-match-patch@1.0.5": { - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + "depd@2.0.0": { + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, - "dotenv@16.4.7": { - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==" + "dotenv@16.6.1": { + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==" }, "dunder-proto@1.0.1": { "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", @@ -617,11 +554,14 @@ "gopd" ] }, + "ee-first@1.1.1": { + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "emoji-regex@8.0.0": { "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "entities@4.5.0": { - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + "encodeurl@2.0.0": { + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "es-define-property@1.0.1": { "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" @@ -644,9 +584,9 @@ "hasown" ] }, - "esbuild@0.25.0": { - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dependencies": [ + "esbuild@0.28.1": { + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "optionalDependencies": [ "@esbuild/aix-ppc64", "@esbuild/android-arm", "@esbuild/android-arm64", @@ -668,23 +608,29 @@ "@esbuild/netbsd-x64", "@esbuild/openbsd-arm64", "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", "@esbuild/sunos-x64", "@esbuild/win32-arm64", "@esbuild/win32-ia32", "@esbuild/win32-x64" - ] + ], + "scripts": true, + "bin": true }, - "esm-env@1.2.2": { - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" + "escape-html@1.0.3": { + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "esrap@1.4.5": { - "integrity": "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==", + "esquery@1.7.0": { + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dependencies": [ - "@jridgewell/sourcemap-codec" + "estraverse" ] }, - "estree-walker@2.0.2": { - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + "estraverse@5.3.0": { + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "etag@1.8.1": { + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "event-target-shim@5.0.1": { "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" @@ -692,6 +638,39 @@ "eventsource-parser@1.1.2": { "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==" }, + "express@5.2.1": { + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": [ + "accepts", + "body-parser", + "content-disposition", + "content-type@1.0.5", + "cookie", + "cookie-signature", + "debug", + "depd", + "encodeurl", + "escape-html", + "etag", + "finalhandler", + "fresh", + "http-errors", + "merge-descriptors", + "mime-types@3.0.2", + "on-finished", + "once", + "parseurl", + "proxy-addr", + "qs", + "range-parser", + "router", + "send", + "serve-static", + "statuses", + "type-is", + "vary" + ] + }, "fast-deep-equal@3.1.3": { "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, @@ -701,6 +680,17 @@ "fft.js@4.0.4": { "integrity": "sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==" }, + "finalhandler@2.1.1": { + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": [ + "debug", + "encodeurl", + "escape-html", + "on-finished", + "parseurl", + "statuses" + ] + }, "form-data-encoder@1.7.2": { "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" }, @@ -710,7 +700,7 @@ "asynckit", "combined-stream", "es-set-tostringtag", - "mime-types" + "mime-types@2.1.35" ] }, "formdata-node@4.4.1": { @@ -720,6 +710,12 @@ "web-streams-polyfill@4.0.0-beta.3" ] }, + "forwarded@0.2.0": { + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh@2.0.0": { + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, "function-bind@1.1.2": { "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, @@ -748,12 +744,6 @@ "gopd@1.2.0": { "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, - "graceful-fs@4.2.11": { - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "has-flag@4.0.0": { - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, "has-symbols@1.1.0": { "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, @@ -769,26 +759,45 @@ "function-bind" ] }, + "http-errors@2.0.1": { + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": [ + "depd", + "inherits", + "setprototypeof", + "statuses", + "toidentifier" + ] + }, "humanize-ms@1.2.1": { "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "dependencies": [ "ms" ] }, + "iconv-lite@0.7.3": { + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dependencies": [ + "safer-buffer" + ] + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "install@0.13.0": { "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==" }, + "ipaddr.js@1.9.1": { + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-any-array@2.0.1": { "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==" }, "is-fullwidth-code-point@3.0.0": { "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "is-reference@3.0.3": { - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dependencies": [ - "@types/estree" - ] + "is-promise@4.0.0": { + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, "js-levenshtein@1.1.6": { "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==" @@ -797,22 +806,12 @@ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": [ "argparse" - ] + ], + "bin": true }, "json-schema-traverse@1.0.0": { "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, - "json-schema@0.4.0": { - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "jsondiffpatch@0.6.0": { - "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", - "dependencies": [ - "@types/diff-match-patch", - "chalk@5.4.1", - "diff-match-patch" - ] - }, "linear-sum-assignment@1.0.7": { "integrity": "sha512-jfLoSGwZNyjfY8eK4ayhjfcIu3BfWvP6sWieYzYI3AWldwXVoWEz1gtrQL10v/8YltYLBunqNjeVFXPMUs+MJg==", "dependencies": [ @@ -822,29 +821,38 @@ "ml-spectra-processing" ] }, - "locate-character@3.0.0": { - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" - }, - "magic-string@0.30.17": { - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dependencies": [ - "@jridgewell/sourcemap-codec" - ] - }, "math-intrinsics@1.1.0": { "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, + "media-typer@1.1.1": { + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==" + }, + "merge-descriptors@2.0.0": { + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" + }, + "meriyah@6.1.4": { + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==" + }, "mime-db@1.52.0": { "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, + "mime-db@1.54.0": { + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, "mime-types@2.1.35": { "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": [ - "mime-db" + "mime-db@1.52.0" ] }, - "minimatch@9.0.5": { - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "mime-types@3.0.2": { + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": [ + "mime-db@1.54.0" + ] + }, + "minimatch@10.2.6": { + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dependencies": [ "brace-expansion" ] @@ -890,17 +898,25 @@ "ml-xsadd@3.0.1": { "integrity": "sha512-Fz2q6dwgzGM8wYKGArTUTZDGa4lQFA2Vi6orjGeTVRy22ZnQFKlJuwS9n8NRviqz1KHAHAzdKJwbnYhdo38uYg==" }, + "module-details-from-path@1.0.4": { + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==" + }, "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "mustache@4.2.0": { - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==" + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": true }, - "nanoid@3.3.9": { - "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==" + "negotiator@1.1.0": { + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dependencies": [ + "content-type@2.1.0" + ] }, "node-domexception@1.0.0": { - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": true }, "node-fetch@2.7.0": { "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", @@ -908,10 +924,28 @@ "whatwg-url" ] }, + "object-assign@4.1.1": { + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect@1.13.4": { + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + }, + "on-finished@2.4.1": { + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": [ + "ee-first" + ] + }, + "once@1.4.0": { + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": [ + "wrappy" + ] + }, "openai@4.47.1": { "integrity": "sha512-WWSxhC/69ZhYWxH/OBsLEirIjUcfpQ5+ihkXKp06hmeYXgBBIUCa9IptMzYx6NdkiOCsSGYCnTIsxaic3AjRCQ==", "dependencies": [ - "@types/node@18.19.80", + "@types/node", "@types/node-fetch", "abort-controller", "agentkeepalive", @@ -919,7 +953,8 @@ "formdata-node", "node-fetch", "web-streams-polyfill@3.3.3" - ] + ], + "bin": true }, "openapi3-ts@4.4.0": { "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", @@ -927,129 +962,187 @@ "yaml" ] }, - "picocolors@1.1.1": { - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + "parseurl@1.3.3": { + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp@8.4.2": { + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==" + }, + "picomatch@4.0.7": { + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==" }, "pluralize@8.0.0": { "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==" }, - "postcss@8.5.3": { - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "proxy-addr@2.0.7": { + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": [ + "forwarded", + "ipaddr.js" + ] + }, + "qs@6.16.0": { + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dependencies": [ - "nanoid", - "picocolors", - "source-map-js" + "es-define-property", + "side-channel" ] }, - "react@19.0.0": { - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==" + "range-parser@1.3.0": { + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==" + }, + "raw-body@3.0.2": { + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": [ + "bytes", + "http-errors", + "iconv-lite", + "unpipe" + ] }, "require-from-string@2.0.2": { "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, - "secure-json-parse@2.7.0": { - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" - }, - "simple-git@3.27.0": { - "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", + "router@2.2.0": { + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dependencies": [ - "@kwsites/file-exists", - "@kwsites/promise-deferred", - "debug" + "debug", + "depd", + "is-promise", + "parseurl", + "path-to-regexp" ] }, - "slugify@1.6.6": { - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==" - }, - "source-map-js@1.2.1": { - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + "safer-buffer@2.1.2": { + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "source-map@0.7.4": { - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + "semifies@1.0.0": { + "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==" }, - "sswr@2.2.0_svelte@5.22.6__acorn@8.14.1": { - "integrity": "sha512-clTszLPZkmycALTHD1mXGU+mOtA/MIoLgS1KGTTzFNVm9rytQVykgRaP+z1zl572cz0bTqj4rFVoC2N+IGK4Sg==", + "send@1.2.1": { + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dependencies": [ - "svelte", - "swrev" + "debug", + "encodeurl", + "escape-html", + "etag", + "fresh", + "http-errors", + "mime-types@3.0.2", + "ms", + "on-finished", + "range-parser", + "statuses" ] }, - "string-width@4.2.3": { - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "serve-static@2.2.1": { + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dependencies": [ - "emoji-regex", - "is-fullwidth-code-point", - "strip-ansi" + "encodeurl", + "escape-html", + "parseurl", + "send" ] }, - "strip-ansi@6.0.1": { - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "setprototypeof@1.2.0": { + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel-list@1.0.1": { + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dependencies": [ - "ansi-regex" + "es-errors", + "object-inspect" ] }, - "supports-color@7.2.0": { - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "side-channel-map@1.0.1": { + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dependencies": [ - "has-flag" + "call-bound", + "es-errors", + "get-intrinsic", + "object-inspect" ] }, - "svelte@5.22.6_acorn@8.14.1": { - "integrity": "sha512-dxHyh3USJyayafSt5I5QD7KuoCM5ZGdIOtLQiKHEro7tymdh0jMcNkiSBVHW+LOA2jEqZEHhyfwN6/pCjx0Fug==", + "side-channel-weakmap@1.0.2": { + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dependencies": [ - "@ampproject/remapping", - "@jridgewell/sourcemap-codec", - "@sveltejs/acorn-typescript", - "@types/estree", - "acorn", - "aria-query", - "axobject-query", - "clsx", - "esm-env", - "esrap", - "is-reference", - "locate-character", - "magic-string", - "zimmerframe" + "call-bound", + "es-errors", + "get-intrinsic", + "object-inspect", + "side-channel-map" ] }, - "swr@2.3.3_react@19.0.0": { - "integrity": "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==", + "side-channel@1.1.1": { + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dependencies": [ - "dequal", - "react", - "use-sync-external-store" + "es-errors", + "object-inspect", + "side-channel-list", + "side-channel-map", + "side-channel-weakmap" ] }, - "swrev@4.0.0": { - "integrity": "sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==" + "source-map@0.7.6": { + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==" + }, + "statuses@2.0.2": { + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, + "string-width@4.2.3": { + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": [ + "emoji-regex", + "is-fullwidth-code-point", + "strip-ansi" + ] }, - "swrv@1.1.0_vue@3.5.13": { - "integrity": "sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==", + "strip-ansi@6.0.1": { + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": [ - "vue" + "ansi-regex" ] }, - "throttleit@2.1.0": { - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==" + "termi-link@1.1.0": { + "integrity": "sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==" + }, + "toidentifier@1.0.1": { + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "tr46@0.0.3": { "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "type-is@2.1.0": { + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dependencies": [ + "content-type@2.1.0", + "media-typer", + "mime-types@3.0.2" + ] + }, "undici-types@5.26.5": { "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "undici-types@6.20.0": { - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "unpipe@1.0.0": { + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, - "use-sync-external-store@1.4.0_react@19.0.0": { - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "unplugin@2.3.11": { + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", "dependencies": [ - "react" + "@jridgewell/remapping", + "acorn", + "picomatch", + "webpack-virtual-modules" ] }, + "uuid@11.1.1": { + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "bin": true + }, "uuid@9.0.1": { - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": true, + "bin": true }, "validate.io-array@1.0.6": { "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==" @@ -1057,15 +1150,8 @@ "validate.io-function@1.0.2": { "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==" }, - "vue@3.5.13": { - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", - "dependencies": [ - "@vue/compiler-dom", - "@vue/compiler-sfc", - "@vue/runtime-dom", - "@vue/server-renderer", - "@vue/shared" - ] + "vary@1.1.2": { + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "web-streams-polyfill@3.3.3": { "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" @@ -1076,6 +1162,9 @@ "webidl-conversions@3.0.1": { "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "webpack-virtual-modules@0.6.2": { + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==" + }, "whatwg-url@5.0.0": { "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dependencies": [ @@ -1083,20 +1172,24 @@ "webidl-conversions" ] }, + "wrappy@1.0.2": { + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, "yaml@2.7.0": { - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==" + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "bin": true }, - "zimmerframe@1.1.2": { - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" + "zod-to-json-schema@3.25.2_zod@3.24.2": { + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==" }, - "zod-to-json-schema@3.24.3_zod@3.24.2": { - "integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==", + "zod-to-json-schema@3.25.2_zod@3.25.76": { + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "dependencies": [ "zod" ] }, - "zod@3.24.2": { - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==" + "zod@3.25.76": { + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" } }, "redirects": { @@ -1114,8 +1207,7 @@ "jsr:@std/fs@^1.0.14", "jsr:@std/path@^1.0.8", "npm:autoevals@^0.0.122", - "npm:braintrust@^0.0.187", - "npm:zod@^3.24.2" + "npm:zod@^3.25.34" ] } } diff --git a/integrations/val.town/vals/tutorial/code.ts b/integrations/val.town/vals/tutorial/code.ts index d4f8e830c..14d1f98fd 100644 --- a/integrations/val.town/vals/tutorial/code.ts +++ b/integrations/val.town/vals/tutorial/code.ts @@ -1,5 +1,5 @@ import { LevenshteinScorer } from "npm:autoevals"; -import { Eval } from "npm:braintrust"; +import { Eval } from "npm:braintrust@^3.30.0"; Eval("Say Hi Bot", { data: () => { diff --git a/js/package.json b/js/package.json index f67c7c68e..d4e150877 100644 --- a/js/package.json +++ b/js/package.json @@ -133,7 +133,6 @@ "test:otel-no-deps": "vitest run src/otel/otel-no-deps.test.ts --reporter=verbose", "test:ai-sdk-v1": "vitest run src/wrappers/ai-sdk-v1.test.ts", "test:ai-sdk-v2": "vitest run src/wrappers/ai-sdk-v2.test.ts src/wrappers/ai-sdk-v1.test.ts", - "test:ai-sdk-v3": "vitest run src/wrappers/ai-sdk-v3.test.ts", "test:mastra": "vitest run src/wrappers/mastra.test.ts", "test:vitest": "pnpm --filter @braintrust/vitest-wrapper-tests test", "bench": "tsx src/queue.bench.ts", diff --git a/js/scripts/pull-vendor.sh b/js/scripts/pull-vendor.sh index d5c7844a9..2fee7812f 100755 --- a/js/scripts/pull-vendor.sh +++ b/js/scripts/pull-vendor.sh @@ -31,7 +31,7 @@ show_usage() { echo " versions - Comma-separated list of major versions to track (e.g., v3,v4,v5)" echo "" echo "Examples:" - echo " $0 https://github.com/vercel/ai.git v3,v4,v5" + echo " $0 https://github.com/vercel/ai.git v4,v5,v6,v7" echo " $0 https://github.com/openai/openai-node.git v3,v4" echo " $0 https://github.com/anthropics/anthropic-sdk-typescript.git v0.20,v0.21" echo "" @@ -285,7 +285,7 @@ Run the pull script to update to the latest tags: cd sdk/js # Example for vercel/ai (uses ai@X.Y.Z tag format) -./scripts/pull-vendor.sh https://github.com/vercel/ai.git v3,v4,v5,v6 +./scripts/pull-vendor.sh https://github.com/vercel/ai.git v4,v5,v6,v7 # Example for OpenAI SDK ./scripts/pull-vendor.sh https://github.com/openai/openai-node.git v3,v4 diff --git a/js/src/auto-instrumentations/configs/ai-sdk.ts b/js/src/auto-instrumentations/configs/ai-sdk.ts index c33be8800..858a8526c 100644 --- a/js/src/auto-instrumentations/configs/ai-sdk.ts +++ b/js/src/auto-instrumentations/configs/ai-sdk.ts @@ -39,31 +39,26 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ }, })), - // generateText - async function - { - channelName: aiSDKChannels.generateText.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <7.0.0", - filePath: "dist/index.mjs", - }, - functionQuery: { - functionName: "generateText", - kind: "Async", - }, - }, - { - channelName: aiSDKChannels.generateText.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <7.0.0", - filePath: "dist/index.js", - }, - functionQuery: { - functionName: "generateText", - kind: "Async", - }, - }, + // Async generation and embedding functions supported by AI SDK v4-v6. + ...[ + ["generateText", aiSDKChannels.generateText.channelName], + ["generateObject", aiSDKChannels.generateObject.channelName], + ["embed", aiSDKChannels.embed.channelName], + ["embedMany", aiSDKChannels.embedMany.channelName], + ].flatMap(([functionName, channelName]) => + ["dist/index.mjs", "dist/index.js"].map((filePath) => ({ + channelName, + module: { + name: "ai", + versionRange: ">=4.0.0 <7.0.0", + filePath, + }, + functionQuery: { + functionName, + kind: "Async" as const, + }, + })), + ), // generateImage - stable in v6+, experimental alias in v5 ...[ @@ -93,135 +88,24 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ })), ), - // streamText - async function (v3 only, before the sync refactor in v4) - { - channelName: aiSDKChannels.streamText.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <4.0.0", - filePath: "dist/index.mjs", - }, - functionQuery: { - functionName: "streamText", - kind: "Async", - }, - }, - - // streamText - sync function returning stream (v4+) - { - channelName: aiSDKChannels.streamTextSync.channelName, - module: { - name: "ai", - versionRange: ">=4.0.0 <7.0.0", - filePath: "dist/index.mjs", - }, - functionQuery: { - functionName: "streamText", - kind: "Sync", - }, - }, - { - channelName: aiSDKChannels.streamText.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <4.0.0", - filePath: "dist/index.js", - }, - functionQuery: { - functionName: "streamText", - kind: "Async", - }, - }, - { - channelName: aiSDKChannels.streamTextSync.channelName, - module: { - name: "ai", - versionRange: ">=4.0.0 <7.0.0", - filePath: "dist/index.js", - }, - functionQuery: { - functionName: "streamText", - kind: "Sync", - }, - }, - - // generateObject - async function - { - channelName: aiSDKChannels.generateObject.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <7.0.0", - filePath: "dist/index.mjs", - }, - functionQuery: { - functionName: "generateObject", - kind: "Async", - }, - }, - { - channelName: aiSDKChannels.generateObject.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <7.0.0", - filePath: "dist/index.js", - }, - functionQuery: { - functionName: "generateObject", - kind: "Async", - }, - }, - - // embed - async function - { - channelName: aiSDKChannels.embed.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <7.0.0", - filePath: "dist/index.mjs", - }, - functionQuery: { - functionName: "embed", - kind: "Async", - }, - }, - { - channelName: aiSDKChannels.embed.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <7.0.0", - filePath: "dist/index.js", - }, - functionQuery: { - functionName: "embed", - kind: "Async", - }, - }, - - // embedMany - async function - { - channelName: aiSDKChannels.embedMany.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <7.0.0", - filePath: "dist/index.mjs", - }, - functionQuery: { - functionName: "embedMany", - kind: "Async", - }, - }, - { - channelName: aiSDKChannels.embedMany.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <7.0.0", - filePath: "dist/index.js", - }, - functionQuery: { - functionName: "embedMany", - kind: "Async", - }, - }, + // Streaming functions are synchronous starting in AI SDK v4. + ...[ + ["streamText", aiSDKChannels.streamTextSync.channelName], + ["streamObject", aiSDKChannels.streamObjectSync.channelName], + ].flatMap(([functionName, channelName]) => + ["dist/index.mjs", "dist/index.js"].map((filePath) => ({ + channelName, + module: { + name: "ai", + versionRange: ">=4.0.0 <7.0.0", + filePath, + }, + functionQuery: { + functionName, + kind: "Sync" as const, + }, + })), + ), // rerank - async function { @@ -277,58 +161,6 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ }, }, - // streamObject - async function (v3 only, before the sync refactor in v4) - { - channelName: aiSDKChannels.streamObject.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <4.0.0", - filePath: "dist/index.mjs", - }, - functionQuery: { - functionName: "streamObject", - kind: "Async", - }, - }, - - // streamObject - sync function returning stream (v4+) - { - channelName: aiSDKChannels.streamObjectSync.channelName, - module: { - name: "ai", - versionRange: ">=4.0.0 <7.0.0", - filePath: "dist/index.mjs", - }, - functionQuery: { - functionName: "streamObject", - kind: "Sync", - }, - }, - { - channelName: aiSDKChannels.streamObject.channelName, - module: { - name: "ai", - versionRange: ">=3.0.0 <4.0.0", - filePath: "dist/index.js", - }, - functionQuery: { - functionName: "streamObject", - kind: "Async", - }, - }, - { - channelName: aiSDKChannels.streamObjectSync.channelName, - module: { - name: "ai", - versionRange: ">=4.0.0 <7.0.0", - filePath: "dist/index.js", - }, - functionQuery: { - functionName: "streamObject", - kind: "Sync", - }, - }, - // Agent.generate - async method (v5 only) // The compiled AI SDK bundle emits this as an anonymous class method, so we // target the first async `generate` method in the file instead of a class name. diff --git a/js/src/instrumentation/providers/ai-sdk-channels.ts b/js/src/instrumentation/providers/ai-sdk-channels.ts index ea3f4a880..4f17cf8dd 100644 --- a/js/src/instrumentation/providers/ai-sdk-channels.ts +++ b/js/src/instrumentation/providers/ai-sdk-channels.ts @@ -47,15 +47,6 @@ export const aiSDKChannels = defineChannels( channelName: "generateImage", kind: "async", }), - streamText: channel< - [AISDKCallParams], - AISDKStreamResult, - AISDKChannelContext, - unknown - >({ - channelName: "streamText", - kind: "async", - }), streamTextSync: channel< [AISDKCallParams], AISDKResult, @@ -74,15 +65,6 @@ export const aiSDKChannels = defineChannels( channelName: "generateObject", kind: "async", }), - streamObject: channel< - [AISDKCallParams], - AISDKStreamResult, - AISDKChannelContext, - unknown - >({ - channelName: "streamObject", - kind: "async", - }), streamObjectSync: channel< [AISDKCallParams], AISDKResult, diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts index 1921dfa02..2250168b2 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts @@ -133,7 +133,7 @@ describe("AI SDK streaming instrumentation", () => { prompt: "Say hello.", maxOutputTokens: 16, }; - const result = (await aiSDKChannels.streamText.tracePromise( + const result = (await aiSDKChannels.generateText.tracePromise( async () => params.model.doStream(params), { arguments: [params], @@ -196,7 +196,7 @@ describe("AI SDK streaming instrumentation", () => { prompt: "Say hello.", maxOutputTokens: 16, }; - const result = (await aiSDKChannels.streamText.tracePromise( + const result = (await aiSDKChannels.generateText.tracePromise( async () => params.model.doStream(params), { arguments: [params], @@ -829,8 +829,8 @@ describe("AI SDK streaming instrumentation", () => { const contentDelayMs = 80; let sentContent = false; - const result = (await aiSDKChannels.streamText.tracePromise( - async () => ({ + const result = aiSDKChannels.streamTextSync.traceSync( + () => ({ baseStream: new ReadableStream({ start(controller) { controller.enqueue({ type: "stream-start", warnings: [] }); @@ -866,7 +866,7 @@ describe("AI SDK streaming instrumentation", () => { }, ], } as any, - )) as any; + ) as any; const reader = result.baseStream.getReader(); while (true) { @@ -890,8 +890,8 @@ describe("AI SDK streaming instrumentation", () => { expect(await backgroundLogger.drain()).toHaveLength(0); let chunkSent = false; - const result = (await aiSDKChannels.streamText.tracePromise( - async () => { + const result = aiSDKChannels.streamTextSync.traceSync( + () => { const resultRecord = { baseStream: new ReadableStream({ pull(controller) { @@ -939,7 +939,7 @@ describe("AI SDK streaming instrumentation", () => { }, ], } as any, - )) as any; + ) as any; expect(Object.getOwnPropertyDescriptor(result, "textStream")?.get).toEqual( expect.any(Function), @@ -962,8 +962,8 @@ describe("AI SDK streaming instrumentation", () => { test("async iterable stream accessors preserve ReadableStream methods", async () => { expect(await backgroundLogger.drain()).toHaveLength(0); - const result = (await aiSDKChannels.streamText.tracePromise( - async () => { + const result = aiSDKChannels.streamTextSync.traceSync( + () => { const resultRecord = { stream: new ReadableStream({ start(controller) { @@ -998,7 +998,7 @@ describe("AI SDK streaming instrumentation", () => { }, ], } as any, - )) as any; + ) as any; expect(result.stream.pipeThrough).toEqual(expect.any(Function)); expect(result.stream.getReader).toEqual(expect.any(Function)); diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts index 5fd33dd6b..242eef3f4 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts @@ -879,32 +879,6 @@ describe("AI SDK utility functions", () => { const result = omit(obj, []); expect(result).toEqual({ a: 1, b: 2 }); }); - - it("should handle complex AI SDK paths", () => { - const obj = { - roundtrips: [ - { - request: { body: "sensitive" }, - response: { headers: "sensitive" }, - }, - ], - rawResponse: { headers: "sensitive" }, - }; - const result = omit(obj, [ - "roundtrips[].request.body", - "roundtrips[].response.headers", - "rawResponse.headers", - ]); - expect(result).toEqual({ - roundtrips: [ - { - request: { body: "" }, - response: { headers: "" }, - }, - ], - rawResponse: { headers: "" }, - }); - }); }); describe("extractGetterValues", () => { @@ -1477,16 +1451,16 @@ describe("AI SDK utility functions", () => { it("should omit specified paths", () => { const output = { text: "Hello", - roundtrips: [ + steps: [ { request: { body: "sensitive" }, response: { data: "ok" }, }, ], }; - const result = processAISDKOutput(output, ["roundtrips[].request.body"]); - expect(result.roundtrips[0].request.body).toBe(""); - expect(result.roundtrips[0].response.data).toBe("ok"); + const result = processAISDKOutput(output, ["steps[].request.body"]); + expect(result.steps[0].request.body).toBe(""); + expect(result.steps[0].response.data).toBe("ok"); }); it("preserves user headers fields while omitting configured transport headers", () => { @@ -1539,21 +1513,6 @@ describe("AI SDK utility functions", () => { id: "provider-response-id", }, ], - roundtrips: [ - { - request: { - headers: { authorization: "secret" }, - body: "secret-roundtrip-request-body", - providerPayload: { - headers: { authorization: "nested-roundtrip-request-secret" }, - }, - }, - response: { - headers: { authorization: "secret" }, - id: "roundtrip-response-id", - }, - }, - ], steps: [ { request: { @@ -1607,7 +1566,6 @@ describe("AI SDK utility functions", () => { }); const serializedResult = JSON.stringify(result); expect(serializedResult).not.toContain("secret-request-body"); - expect(serializedResult).not.toContain("secret-roundtrip-request-body"); expect(serializedResult).not.toContain("secret-step-request-body"); expect(serializedResult).not.toContain("authorization"); expect(result.request).not.toHaveProperty("headers"); @@ -1623,13 +1581,6 @@ describe("AI SDK utility functions", () => { ); expect(result.rawResponse).not.toHaveProperty("headers"); expect(result.responses[0]).not.toHaveProperty("headers"); - if (result.roundtrips) { - expect(result.roundtrips[0].request).not.toHaveProperty("headers"); - expect(result.roundtrips[0].request.providerPayload).not.toHaveProperty( - "headers", - ); - expect(result.roundtrips[0].response).not.toHaveProperty("headers"); - } expect(result.steps[0].request).not.toHaveProperty("headers"); expect(result.steps[0].request.providerPayload).not.toHaveProperty( "headers", diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.ts index ae0093a31..81215040c 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.ts @@ -79,7 +79,7 @@ import { BRAINTRUST_AI_SDK_V7_OPERATION_KEY as AI_SDK_V7_OPERATION_KEY } from ". interface AISDKInstrumentationConfig { /** * List of JSON paths to remove from output field. - * Uses dot notation with array wildcards: "roundtrips[].request.body" + * Uses dot notation with array wildcards: "steps[].request.body" */ denyOutputPaths?: string[]; } @@ -89,10 +89,6 @@ interface AISDKInstrumentationConfig { * These contain redundant or verbose data that's not useful for tracing. */ export const DEFAULT_DENY_OUTPUT_PATHS: string[] = [ - // v3 - "roundtrips[].request.body", - "roundtrips[].request.headers", - "roundtrips[].response.headers", "rawResponse.headers", "responseMessages", // v5 @@ -122,8 +118,6 @@ const TRANSPORT_PAYLOAD_ROOT_PATHS = [ "request", "response", "responses[]", - "roundtrips[].request", - "roundtrips[].response", "steps[].request", "steps[].response", "steps[].responses[]", @@ -232,32 +226,7 @@ class AISDKInstrumentationConsumer { extractMetrics: (result) => extractTokenMetrics(result), }); - // streamText - function returning stream - traceStreamingChannel(aiSDKChannels.streamText, { - name: "streamText", - type: SpanTypeAttribute.FUNCTION, - shouldTrace: () => currentCloudflareThinkSpan() === undefined, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent, startTime), - aggregateChunks: aggregateAISDKChunks, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }); - - // streamText - sync function returning stream (v4+, used by auto-hook) + // streamText - sync function returning stream traceSyncStreamChannel(aiSDKChannels.streamTextSync, { name: "streamText", type: SpanTypeAttribute.FUNCTION, @@ -292,31 +261,7 @@ class AISDKInstrumentationConsumer { aggregateChunks: aggregateAISDKChunks, }); - // streamObject - function returning stream - traceStreamingChannel(aiSDKChannels.streamObject, { - name: "streamObject", - type: SpanTypeAttribute.FUNCTION, - extractInput: ([params], event, span) => - prepareAISDKCallInput(params, event, span, denyOutputPaths), - extractOutput: (result, endEvent) => - processAISDKOutput( - result, - resolveDenyOutputPaths(endEvent, denyOutputPaths), - ), - extractMetrics: (result, startTime, endEvent) => - extractTopLevelAISDKMetrics(result, endEvent, startTime), - aggregateChunks: aggregateAISDKChunks, - patchResult: ({ endEvent, result, span, startTime }) => - patchAISDKStreamingResult({ - defaultDenyOutputPaths: denyOutputPaths, - endEvent, - result, - span, - startTime, - }), - }); - - // streamObject - sync function returning stream (v4+, used by auto-hook) + // streamObject - sync function returning stream traceSyncStreamChannel(aiSDKChannels.streamObjectSync, { name: "streamObject", type: SpanTypeAttribute.FUNCTION, diff --git a/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts index 17751542a..d19a5f95b 100644 --- a/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts +++ b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts @@ -39,9 +39,7 @@ type ThinkRunState = { startTime: number; }; -type AISDKStreamEvent = - | ChannelMessage - | ChannelMessage; +type AISDKStreamEvent = ChannelMessage; const THINK_STATE_ID = Symbol.for("braintrust.cloudflare-think.state-id"); @@ -51,7 +49,6 @@ class CloudflareThinkInstrumentationConsumer { public register(): void { this.subscribeToThinkRuns(); this.subscribeToAISDKStreamTextSync(); - this.subscribeToAISDKStreamTextAsync(); } private subscribeToThinkRuns(): void { @@ -161,25 +158,6 @@ class CloudflareThinkInstrumentationConsumer { channel.subscribe(handlers); } - private subscribeToAISDKStreamTextAsync(): void { - const channel = aiSDKChannels.streamText.tracingChannel(); - const handlers: IsoChannelHandlers< - ChannelMessage - > = { - start: (event) => { - this.startAISDKStream(event); - }, - asyncEnd: (event) => { - this.endAISDKStream(event); - }, - error: (event) => { - this.errorAISDKStream(event); - }, - }; - - channel.subscribe(handlers); - } - private startAISDKStream(event: AISDKStreamEvent): void { const runState = this.currentState(); if (!runState || runState.finalized) { diff --git a/js/src/vendor-sdk-types/ai-sdk-common.ts b/js/src/vendor-sdk-types/ai-sdk-common.ts index 5c8d406ba..a7f814cfc 100644 --- a/js/src/vendor-sdk-types/ai-sdk-common.ts +++ b/js/src/vendor-sdk-types/ai-sdk-common.ts @@ -370,6 +370,9 @@ export interface AISDKProviderResolver { } export interface AISDKNamespaceBase { + Agent?: AISDKAgentClass; + Experimental_Agent?: AISDKAgentClass; + ToolLoopAgent?: AISDKAgentClass; generateText: AISDKGenerateFunction; generateImage?: AISDKGenerateImageFunction; experimental_generateImage?: AISDKGenerateImageFunction; diff --git a/js/src/vendor-sdk-types/ai-sdk-v3.ts b/js/src/vendor-sdk-types/ai-sdk-v3.ts deleted file mode 100644 index 266324d14..000000000 --- a/js/src/vendor-sdk-types/ai-sdk-v3.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { - AISDKGenerateFunction, - AISDKNamespaceBase, - AISDKStreamFunction, -} from "./ai-sdk-common"; - -export interface AISDKV3 extends AISDKNamespaceBase { - experimental_generateText?: AISDKGenerateFunction; - experimental_streamText?: AISDKStreamFunction; - experimental_generateObject?: AISDKGenerateFunction; - experimental_streamObject?: AISDKStreamFunction; -} diff --git a/js/src/vendor-sdk-types/ai-sdk-v4.ts b/js/src/vendor-sdk-types/ai-sdk-v4.ts deleted file mode 100644 index 5aa750e74..000000000 --- a/js/src/vendor-sdk-types/ai-sdk-v4.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { AISDKNamespaceBase } from "./ai-sdk-common"; - -export type AISDKV4 = AISDKNamespaceBase; diff --git a/js/src/vendor-sdk-types/ai-sdk-v5.ts b/js/src/vendor-sdk-types/ai-sdk-v5.ts deleted file mode 100644 index ce1bb3d97..000000000 --- a/js/src/vendor-sdk-types/ai-sdk-v5.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { AISDKAgentClass, AISDKNamespaceBase } from "./ai-sdk-common"; - -export interface AISDKV5 extends AISDKNamespaceBase { - Agent?: AISDKAgentClass; - Experimental_Agent?: AISDKAgentClass; -} diff --git a/js/src/vendor-sdk-types/ai-sdk-v6.ts b/js/src/vendor-sdk-types/ai-sdk-v6.ts deleted file mode 100644 index 339cc07b4..000000000 --- a/js/src/vendor-sdk-types/ai-sdk-v6.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { AISDKAgentClass, AISDKNamespaceBase } from "./ai-sdk-common"; - -export interface AISDKV6 extends AISDKNamespaceBase { - Agent?: AISDKAgentClass; - Experimental_Agent?: AISDKAgentClass; - ToolLoopAgent?: AISDKAgentClass; -} diff --git a/js/src/vendor-sdk-types/ai-sdk.ts b/js/src/vendor-sdk-types/ai-sdk.ts index 4f36a0d22..cbf304a43 100644 --- a/js/src/vendor-sdk-types/ai-sdk.ts +++ b/js/src/vendor-sdk-types/ai-sdk.ts @@ -22,6 +22,7 @@ import type { AISDKModelStreamChunk, AISDKOutputObject, AISDKOutputResponseFormat, + AISDKNamespaceBase, AISDKRerankFunction, AISDKRerankParams, AISDKRerankResult, @@ -32,18 +33,7 @@ import type { AISDKUsage, AISDKWorkflowAgentClass, } from "./ai-sdk-common"; -import type { AISDKV3 } from "./ai-sdk-v3"; -import type { AISDKV4 } from "./ai-sdk-v4"; -import type { AISDKV5 } from "./ai-sdk-v5"; -import type { AISDKV6 } from "./ai-sdk-v6"; - -export type AISDKVersion = - | { majorVersion: 3; sdk: AISDKV3 } - | { majorVersion: 4; sdk: AISDKV4 } - | { majorVersion: 5; sdk: AISDKV5 } - | { majorVersion: 6; sdk: AISDKV6 }; - -export type AISDK = AISDKVersion["sdk"]; +export type AISDK = AISDKNamespaceBase; export type { AISDKAgentClass, diff --git a/js/src/wrappers/ai-sdk/ai-sdk.ts b/js/src/wrappers/ai-sdk/ai-sdk.ts index 396fa733e..52184723f 100644 --- a/js/src/wrappers/ai-sdk/ai-sdk.ts +++ b/js/src/wrappers/ai-sdk/ai-sdk.ts @@ -625,8 +625,8 @@ const wrapRerank = ( const makeStreamWrapper = ( channel: - | typeof aiSDKChannels.streamText - | typeof aiSDKChannels.streamObject + | typeof aiSDKChannels.streamTextSync + | typeof aiSDKChannels.streamObjectSync | typeof aiSDKChannels.agentStream | typeof harnessAgentChannels.stream | typeof harnessAgentChannels.continueStream @@ -656,6 +656,10 @@ const makeStreamWrapper = ( }), }); + if ("traceSync" in channel) { + return channel.traceSync(() => streamText(tracedParams) as any, context); + } + return channel.tracePromise(() => streamText(tracedParams) as any, context); }; Object.defineProperty(wrapper, "name", { value: name, writable: false }); @@ -668,7 +672,7 @@ const wrapStreamText = ( aiSDK?: AISDK, ) => { return makeStreamWrapper( - aiSDKChannels.streamText, + aiSDKChannels.streamTextSync, "streamText", streamText, { aiSDK }, @@ -682,7 +686,7 @@ const wrapStreamObject = ( aiSDK?: AISDK, ) => { return makeStreamWrapper( - aiSDKChannels.streamObject, + aiSDKChannels.streamObjectSync, "streamObject", streamObject, { aiSDK }, From 5ed64b2ebd692cad965f99692a097b5b6be2bd9d Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 14 Sep 2026 15:47:34 +0200 Subject: [PATCH 10/19] chore!: Remove deprecated packages (langchain, openai agents, ai sdk) (#2485) Closes [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) --- .changeset/reduce-api-to-a-minimum.md | 2 - README.md | 14 +- e2e/README.md | 2 +- e2e/config/pr-comment-scenarios.json | 15 - e2e/package.json | 1 - ...angchain-js-traces-v1-latest.cassette.json | 673 -------- .../wrap-langchain-js-traces-v1.cassette.json | 673 -------- ...ngchain-js-traces-v1-latest.span-tree.json | 1511 ----------------- ...angchain-js-traces-v1-latest.span-tree.txt | 1450 ---------------- ...wrap-langchain-js-traces-v1.span-tree.json | 1511 ----------------- .../wrap-langchain-js-traces-v1.span-tree.txt | 1450 ---------------- .../wrap-langchain-js-traces/assertions.ts | 113 -- .../wrap-langchain-js-traces/package.json | 31 - .../wrap-langchain-js-traces/pnpm-lock.yaml | 296 ---- .../wrap-langchain-js-traces/scenario.test.ts | 76 - .../wrap-langchain-js-traces/scenario.ts | 162 -- integrations/langchain-js/CHANGELOG.md | 31 - integrations/langchain-js/README.md | 59 - integrations/langchain-js/package.json | 64 - .../src/BraintrustCallbackHandler.fixtures.ts | 234 --- .../src/BraintrustCallbackHandler.test.ts | 1239 -------------- .../src/BraintrustCallbackHandler.ts | 254 --- integrations/langchain-js/src/index.ts | 2 - .../langchain-js/src/setGlobalHandler.test.ts | 138 -- .../langchain-js/src/setGlobalHandler.ts | 24 - integrations/langchain-js/src/test/setup.ts | 51 - integrations/langchain-js/src/test/types.ts | 32 - integrations/langchain-js/src/test/utils.ts | 72 - integrations/langchain-js/tsconfig.json | 15 - integrations/langchain-js/tsup.config.ts | 11 - integrations/langchain-js/turbo.json | 8 - integrations/langchain-js/vitest.config.ts | 9 - integrations/openai-agents-js/CHANGELOG.md | 20 - integrations/openai-agents-js/README.md | 74 - integrations/openai-agents-js/package.json | 50 - integrations/openai-agents-js/src/index.ts | 675 -------- .../src/openai-agents-integration.test.ts | 1264 -------------- .../src/openai-agents-trace-processor.test.ts | 288 ---- .../src/openai-agents-types.ts | 150 -- .../openai-agents-js/src/test-logger.d.ts | 12 - integrations/openai-agents-js/src/types.ts | 120 -- integrations/openai-agents-js/tsconfig.json | 15 - integrations/openai-agents-js/tsup.config.ts | 10 - integrations/openai-agents-js/turbo.json | 12 - .../openai-agents-js/vitest.config.ts | 20 - integrations/vercel-ai-sdk/CHANGELOG.md | 35 - integrations/vercel-ai-sdk/README.md | 24 - integrations/vercel-ai-sdk/package.json | 48 - .../vercel-ai-sdk/src/adapter.test.ts | 87 - integrations/vercel-ai-sdk/src/adapter.ts | 128 -- integrations/vercel-ai-sdk/src/index.ts | 6 - integrations/vercel-ai-sdk/tsconfig.json | 15 - integrations/vercel-ai-sdk/tsup.config.ts | 10 - integrations/vercel-ai-sdk/turbo.json | 8 - integrations/vercel-ai-sdk/vitest.config.ts | 8 - pnpm-lock.yaml | 1281 +------------- scripts/release/_shared.mjs | 6 - 57 files changed, 55 insertions(+), 14534 deletions(-) delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/__cassettes__/wrap-langchain-js-traces-v1-latest.cassette.json delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/__cassettes__/wrap-langchain-js-traces-v1.cassette.json delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1-latest.span-tree.json delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1-latest.span-tree.txt delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1.span-tree.json delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1.span-tree.txt delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/assertions.ts delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/package.json delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/pnpm-lock.yaml delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/scenario.test.ts delete mode 100644 e2e/scenarios/wrap-langchain-js-traces/scenario.ts delete mode 100644 integrations/langchain-js/CHANGELOG.md delete mode 100644 integrations/langchain-js/README.md delete mode 100644 integrations/langchain-js/package.json delete mode 100644 integrations/langchain-js/src/BraintrustCallbackHandler.fixtures.ts delete mode 100644 integrations/langchain-js/src/BraintrustCallbackHandler.test.ts delete mode 100644 integrations/langchain-js/src/BraintrustCallbackHandler.ts delete mode 100644 integrations/langchain-js/src/index.ts delete mode 100644 integrations/langchain-js/src/setGlobalHandler.test.ts delete mode 100644 integrations/langchain-js/src/setGlobalHandler.ts delete mode 100644 integrations/langchain-js/src/test/setup.ts delete mode 100644 integrations/langchain-js/src/test/types.ts delete mode 100644 integrations/langchain-js/src/test/utils.ts delete mode 100644 integrations/langchain-js/tsconfig.json delete mode 100644 integrations/langchain-js/tsup.config.ts delete mode 100644 integrations/langchain-js/turbo.json delete mode 100644 integrations/langchain-js/vitest.config.ts delete mode 100644 integrations/openai-agents-js/CHANGELOG.md delete mode 100644 integrations/openai-agents-js/README.md delete mode 100644 integrations/openai-agents-js/package.json delete mode 100644 integrations/openai-agents-js/src/index.ts delete mode 100644 integrations/openai-agents-js/src/openai-agents-integration.test.ts delete mode 100644 integrations/openai-agents-js/src/openai-agents-trace-processor.test.ts delete mode 100644 integrations/openai-agents-js/src/openai-agents-types.ts delete mode 100644 integrations/openai-agents-js/src/test-logger.d.ts delete mode 100644 integrations/openai-agents-js/src/types.ts delete mode 100644 integrations/openai-agents-js/tsconfig.json delete mode 100644 integrations/openai-agents-js/tsup.config.ts delete mode 100644 integrations/openai-agents-js/turbo.json delete mode 100644 integrations/openai-agents-js/vitest.config.ts delete mode 100644 integrations/vercel-ai-sdk/CHANGELOG.md delete mode 100644 integrations/vercel-ai-sdk/README.md delete mode 100644 integrations/vercel-ai-sdk/package.json delete mode 100644 integrations/vercel-ai-sdk/src/adapter.test.ts delete mode 100644 integrations/vercel-ai-sdk/src/adapter.ts delete mode 100644 integrations/vercel-ai-sdk/src/index.ts delete mode 100644 integrations/vercel-ai-sdk/tsconfig.json delete mode 100644 integrations/vercel-ai-sdk/tsup.config.ts delete mode 100644 integrations/vercel-ai-sdk/turbo.json delete mode 100644 integrations/vercel-ai-sdk/vitest.config.ts diff --git a/.changeset/reduce-api-to-a-minimum.md b/.changeset/reduce-api-to-a-minimum.md index 958c893c9..e91ea136d 100644 --- a/.changeset/reduce-api-to-a-minimum.md +++ b/.changeset/reduce-api-to-a-minimum.md @@ -1,9 +1,7 @@ --- "braintrust": major "@braintrust/otel": major -"@braintrust/vercel-ai-sdk": major "@braintrust/browser": patch -"@braintrust/openai-agents": patch "@braintrust/temporal": patch --- diff --git a/README.md b/README.md index 6eafe75e0..a8f2c7cdd 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,12 @@ BRAINTRUST_API_KEY= npx bt eval tutorial.eval.ts ## Packages -| Package | Purpose | npm | Docs | -| --------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | -| `braintrust` | Core JavaScript/TypeScript SDK for logging, tracing, evals, and CLI. | [![npm: braintrust](https://img.shields.io/npm/v/braintrust.svg)](https://www.npmjs.com/package/braintrust) | [js/README.md](js/README.md) | -| `@braintrust/browser` | Browser-focused SDK integration with AsyncLocalStorage polyfill support. | [![npm: @braintrust/browser](https://img.shields.io/npm/v/%40braintrust%2Fbrowser.svg)](https://www.npmjs.com/package/@braintrust/browser) | [integrations/browser-js/README.md](integrations/browser-js/README.md) | -| `@braintrust/langchain-js` | Deprecated; use the LangChain integration in `braintrust` instead. | [![npm: @braintrust/langchain-js](https://img.shields.io/npm/v/%40braintrust%2Flangchain-js.svg)](https://www.npmjs.com/package/@braintrust/langchain-js) | [integrations/langchain-js/README.md](integrations/langchain-js/README.md) | -| `@braintrust/openai-agents` | Deprecated; use the OpenAI Agents integration in `braintrust` instead. | [![npm: @braintrust/openai-agents](https://img.shields.io/npm/v/%40braintrust%2Fopenai-agents.svg)](https://www.npmjs.com/package/@braintrust/openai-agents) | [integrations/openai-agents-js/README.md](integrations/openai-agents-js/README.md) | -| `@braintrust/otel` | OpenTelemetry span processor and compatibility helpers for Braintrust tracing. | [![npm: @braintrust/otel](https://img.shields.io/npm/v/%40braintrust%2Fotel.svg)](https://www.npmjs.com/package/@braintrust/otel) | [integrations/otel-js/README.md](integrations/otel-js/README.md) | -| `@braintrust/temporal` | Temporal client/worker plugin and workflow interceptors for Braintrust tracing. | [![npm: @braintrust/temporal](https://img.shields.io/npm/v/%40braintrust%2Ftemporal.svg)](https://www.npmjs.com/package/@braintrust/temporal) | [integrations/temporal-js/README.md](integrations/temporal-js/README.md) | +| Package | Purpose | npm | Docs | +| ---------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `braintrust` | Core JavaScript/TypeScript SDK for logging, tracing, evals, and CLI. | [![npm: braintrust](https://img.shields.io/npm/v/braintrust.svg)](https://www.npmjs.com/package/braintrust) | [js/README.md](js/README.md) | +| `@braintrust/browser` | Browser-focused SDK integration with AsyncLocalStorage polyfill support. | [![npm: @braintrust/browser](https://img.shields.io/npm/v/%40braintrust%2Fbrowser.svg)](https://www.npmjs.com/package/@braintrust/browser) | [integrations/browser-js/README.md](integrations/browser-js/README.md) | +| `@braintrust/otel` | OpenTelemetry span processor and compatibility helpers for Braintrust tracing. | [![npm: @braintrust/otel](https://img.shields.io/npm/v/%40braintrust%2Fotel.svg)](https://www.npmjs.com/package/@braintrust/otel) | [integrations/otel-js/README.md](integrations/otel-js/README.md) | +| `@braintrust/temporal` | Temporal client/worker plugin and workflow interceptors for Braintrust tracing. | [![npm: @braintrust/temporal](https://img.shields.io/npm/v/%40braintrust%2Ftemporal.svg)](https://www.npmjs.com/package/@braintrust/temporal) | [integrations/temporal-js/README.md](integrations/temporal-js/README.md) | ## CLI `bt` diff --git a/e2e/README.md b/e2e/README.md index 47bd2b00f..ceb9110ee 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -188,7 +188,7 @@ After any successful record run, stale cassette variants are cleaned only inside These scenarios have cassette wiring in place and will use cassettes once they're recorded: -`anthropic-bedrock-instrumentation`, `anthropic-instrumentation`, `openai-instrumentation`, `openai-codex-instrumentation`, `ai-sdk-instrumentation`, `ai-sdk-otel-export`, `claude-agent-sdk-instrumentation`, `cohere-instrumentation`, `cursor-sdk-instrumentation`, `github-copilot-instrumentation`, `google-adk-instrumentation`, `google-genai-instrumentation`, `groq-instrumentation`, `huggingface-instrumentation`, `mistral-instrumentation`, `ollama-instrumentation`, `openrouter-agent-instrumentation`, `openrouter-instrumentation`, `wrap-langchain-js-traces`. +`anthropic-bedrock-instrumentation`, `anthropic-instrumentation`, `openai-instrumentation`, `openai-codex-instrumentation`, `ai-sdk-instrumentation`, `ai-sdk-otel-export`, `claude-agent-sdk-instrumentation`, `cohere-instrumentation`, `cursor-sdk-instrumentation`, `github-copilot-instrumentation`, `google-adk-instrumentation`, `google-genai-instrumentation`, `groq-instrumentation`, `huggingface-instrumentation`, `mistral-instrumentation`, `ollama-instrumentation`, `openrouter-agent-instrumentation`, `openrouter-instrumentation`. ### Cassette format diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index f5ba3ad75..0a2d49296 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -744,20 +744,5 @@ "variantKey": "vitest-evals-reporter" } ] - }, - { - "scenarioDirName": "wrap-langchain-js-traces", - "label": "LangChain JS Wrapper Traces", - "metadataScenario": "wrap-langchain-js-traces", - "variants": [ - { - "variantKey": "wrap-langchain-js-traces-v1", - "label": "v1 pinned" - }, - { - "variantKey": "wrap-langchain-js-traces-v1-latest", - "label": "v1 latest" - } - ] } ] diff --git a/e2e/package.json b/e2e/package.json index ba8b71a89..04599f7ab 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -10,7 +10,6 @@ "devDependencies": { "@braintrust/browser": "workspace:^", "@braintrust/deepseek-harness": "workspace:^", - "@braintrust/langchain-js": "workspace:^", "@braintrust/otel": "workspace:^", "@braintrust/seinfeld": "workspace:^", "@opentelemetry/api": ">=1.9.0", diff --git a/e2e/scenarios/wrap-langchain-js-traces/__cassettes__/wrap-langchain-js-traces-v1-latest.cassette.json b/e2e/scenarios/wrap-langchain-js-traces/__cassettes__/wrap-langchain-js-traces-v1-latest.cassette.json deleted file mode 100644 index bce5a3c83..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/__cassettes__/wrap-langchain-js-traces-v1-latest.cassette.json +++ /dev/null @@ -1,673 +0,0 @@ -{ - "entries": [ - { - "callIndex": 0, - "id": "86e93f0671afd2fb", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:32.278Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_completion_tokens": 512, - "messages": [ - { - "content": "Reply with exactly OK.", - "role": "user" - } - ], - "model": "gpt-5-nano", - "stream": false - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "message": { - "annotations": [], - "content": "OK", - "refusal": null, - "role": "assistant" - } - } - ], - "created": 1787574030, - "id": "chatcmpl-EGNx8tOIBqW4PL9inY296btKeUCr1", - "model": "gpt-5-nano-2025-08-07", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": null, - "usage": { - "completion_tokens": 266, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 256, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 11, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 277 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024fb8088a2fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:32 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "2073", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179999994", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_4371b279cf104e0697b031338add78f2" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 1, - "id": "a613a13df4910706", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:32.699Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Reply with the single word PARIS and nothing else.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0 - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": "PARIS", - "refusal": null, - "role": "assistant" - } - } - ], - "created": 1787574032, - "id": "chatcmpl-EGNxAPgSJsXEVlK5YEAjNpYLSUFML", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_fabbcdf5d9", - "usage": { - "completion_tokens": 2, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 18, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 20 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024fc59a5b2fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:32 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "338", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999985", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_6de919b320864952a77010c3682478ac" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 2, - "id": "394c0994869e6e1f", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:33.104Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Count from 1 to 3 and include the words one two three.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": true, - "stream_options": { - "include_usage": true - }, - "temperature": 0 - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "chunks": [ - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"wxWQ0eCDL\"}", - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"One\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"uje9Teyd\"}", - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"B0K64QetnP\"}", - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" two\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"S4dypzF\"}", - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"XdqizbjE0P\"}", - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Bq0Q1\"}", - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"BoOsWxwVOZ\"}", - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"OgqTv\"}", - "data: {\"id\":\"chatcmpl-EGNxAtVgtjTY51DEVaYfresrdBMst\",\"object\":\"chat.completion.chunk\",\"created\":1787574032,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[],\"usage\":{\"prompt_tokens\":22,\"completion_tokens\":6,\"total_tokens\":28,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}},\"obfuscation\":\"3mFwIC92nf0\"}", - "data: [DONE]" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024fc82b622fd0-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 24 Aug 2026 12:20:32 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "244", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999982", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_0be6fc8fedbe4d70805e3b1ab80f25cf" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 3, - "id": "7f1e5cd685579c33", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:33.721Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 128, - "messages": [ - { - "content": "Use the get_weather tool for Paris, France. Do not answer from memory.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Get the current weather in a given location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and state or city and country", - "type": "string" - } - }, - "required": ["location"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "tool_calls", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": null, - "refusal": null, - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"location\":\"Paris, France\"}", - "name": "get_weather" - }, - "id": "call_qjYJazVqODvAan4YaPgZZ8ra", - "type": "function" - } - ] - } - } - ], - "created": 1787574033, - "id": "chatcmpl-EGNxB4gTTflGWk1BHpKwKrYsdlH75", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_02ae59e84c", - "usage": { - "completion_tokens": 16, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 72, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 88 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024fcabbd32fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:33 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "546", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999980", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_ded6c40040e749c49af0e74794677f16" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 4, - "id": "92b304bbb4ce5170", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:34.417Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 128, - "messages": [ - { - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": ["add", "subtract", "multiply", "divide"], - "type": "string" - } - }, - "required": ["operation", "a", "b"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "tool_calls", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": null, - "refusal": null, - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"operation\":\"multiply\",\"a\":127,\"b\":49}", - "name": "calculate" - }, - "id": "call_BMkQ0blzGQeVPEC7pIElPdRs", - "type": "function" - } - ] - } - } - ], - "created": 1787574033, - "id": "chatcmpl-EGNxBqL6zQDAnMT6RdDBaUwkEjbWf", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_0950b7f384", - "usage": { - "completion_tokens": 21, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 76, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 97 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024fce88f42fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:34 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "617", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999985", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_d320b09ebbc6405cab6edb5dc6a5e0c1" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 5, - "id": "6fa54a31e7345249", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:34.924Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 128, - "messages": [ - { - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "role": "user" - }, - { - "content": "", - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"operation\":\"multiply\",\"a\":127,\"b\":49}", - "name": "calculate" - }, - "id": "call_BMkQ0blzGQeVPEC7pIElPdRs", - "type": "function" - } - ] - }, - { - "content": "6223", - "role": "tool", - "tool_call_id": "call_BMkQ0blzGQeVPEC7pIElPdRs" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": ["add", "subtract", "multiply", "divide"], - "type": "string" - } - }, - "required": ["operation", "a", "b"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": "127 multiplied by 49 is 6223.", - "refusal": null, - "role": "assistant" - } - } - ], - "created": 1787574034, - "id": "chatcmpl-EGNxCT0hwMHK5XDv3ViqjOdQ3sShs", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_0950b7f384", - "usage": { - "completion_tokens": 11, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 106, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 117 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024fd2e94a2fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:34 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "444", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999980", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_149ef64cc6b543eaa8b7c0f602c66bea" - }, - "status": 200, - "statusText": "OK" - } - } - ], - "meta": { - "createdAt": "2026-07-06T14:01:28.480Z" - } -} diff --git a/e2e/scenarios/wrap-langchain-js-traces/__cassettes__/wrap-langchain-js-traces-v1.cassette.json b/e2e/scenarios/wrap-langchain-js-traces/__cassettes__/wrap-langchain-js-traces-v1.cassette.json deleted file mode 100644 index 604a9bcdd..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/__cassettes__/wrap-langchain-js-traces-v1.cassette.json +++ /dev/null @@ -1,673 +0,0 @@ -{ - "entries": [ - { - "callIndex": 0, - "id": "86e93f0671afd2fb", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:22.627Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_completion_tokens": 512, - "messages": [ - { - "content": "Reply with exactly OK.", - "role": "user" - } - ], - "model": "gpt-5-nano", - "stream": false - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "message": { - "annotations": [], - "content": "OK", - "refusal": null, - "role": "assistant" - } - } - ], - "created": 1787574021, - "id": "chatcmpl-EGNwzUCt2YhtMXdmxJ7ea1qvFvjhj", - "model": "gpt-5-nano-2025-08-07", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": null, - "usage": { - "completion_tokens": 138, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 128, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 11, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 149 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024f76c8462fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:22 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "1516", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "180000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "179999994", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_35266a070f524ced863bd688a979fb9c" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 1, - "id": "a613a13df4910706", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:23.323Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Reply with the single word PARIS and nothing else.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0 - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": "PARIS", - "refusal": null, - "role": "assistant" - } - } - ], - "created": 1787574023, - "id": "chatcmpl-EGNx181W8ybisMsX1olUNegho55Z4", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_fabbcdf5d9", - "usage": { - "completion_tokens": 2, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 18, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 20 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024f894d132fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:23 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "273", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999985", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_06fe5d162d7f463bb59e50d729b4d20c" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 2, - "id": "394c0994869e6e1f", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:24.024Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 32, - "messages": [ - { - "content": "Count from 1 to 3 and include the words one two three.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": true, - "stream_options": { - "include_usage": true - }, - "temperature": 0 - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "chunks": [ - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Oj4Hh3j31\"}", - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"One\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Il4NOQeI\"}", - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"rVAPk2Cs7X\"}", - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" two\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"sFT4iNt\"}", - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"tSa35AR216\"}", - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"oXtaZ\"}", - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"pfwlwCc1UG\"}", - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"0pszW\"}", - "data: {\"id\":\"chatcmpl-EGNx1I8x3YZKvOGhuC0S5biZ26Gwl\",\"object\":\"chat.completion.chunk\",\"created\":1787574023,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_0950b7f384\",\"choices\":[],\"usage\":{\"prompt_tokens\":22,\"completion_tokens\":6,\"total_tokens\":28,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}},\"obfuscation\":\"kalzk5DmKv4\"}", - "data: [DONE]" - ], - "kind": "sse" - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024f8d9d6b2fd0-IAD", - "connection": "keep-alive", - "content-type": "text/event-stream; charset=utf-8", - "date": "Mon, 24 Aug 2026 12:20:23 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "396", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999985", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_a0055ad7660e45bb855ae0977d3a9b46" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 3, - "id": "7f1e5cd685579c33", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:24.773Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 128, - "messages": [ - { - "content": "Use the get_weather tool for Paris, France. Do not answer from memory.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Get the current weather in a given location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and state or city and country", - "type": "string" - } - }, - "required": ["location"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "tool_calls", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": null, - "refusal": null, - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"location\":\"Paris, France\"}", - "name": "get_weather" - }, - "id": "call_vFUhlOlK88FODFbm0RCSfovD", - "type": "function" - } - ] - } - } - ], - "created": 1787574024, - "id": "chatcmpl-EGNx23GX8lOwzBQnZTCKTXwLeUyRG", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_02ae59e84c", - "usage": { - "completion_tokens": 16, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 72, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 88 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024f91fd8b2fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:24 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "649", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999977", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_27d9929b8cfa4c40b969771b87ffd0bf" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 4, - "id": "92b304bbb4ce5170", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:27.515Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 128, - "messages": [ - { - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "role": "user" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": ["add", "subtract", "multiply", "divide"], - "type": "string" - } - }, - "required": ["operation", "a", "b"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "tool_calls", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": null, - "refusal": null, - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"operation\":\"multiply\",\"a\":127,\"b\":49}", - "name": "calculate" - }, - "id": "call_dXQI5d9UAYuRygDqNMunj2W2", - "type": "function" - } - ] - } - } - ], - "created": 1787574027, - "id": "chatcmpl-EGNx52wzdb9GL8hdL3YZCMSLvJkQ4", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_0950b7f384", - "usage": { - "completion_tokens": 21, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 76, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 97 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024f969f342fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:27 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "729", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999985", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_86b70fed7a674d028ec1293c0e2cd0ec" - }, - "status": 200, - "statusText": "OK" - } - }, - { - "callIndex": 5, - "id": "a57d0dfe4d90c4bd", - "matchKey": "POST api.openai.com/v1/chat/completions", - "recordedAt": "2026-08-24T12:20:28.216Z", - "request": { - "body": { - "kind": "json", - "value": { - "max_tokens": 128, - "messages": [ - { - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "role": "user" - }, - { - "content": "", - "role": "assistant", - "tool_calls": [ - { - "function": { - "arguments": "{\"operation\":\"multiply\",\"a\":127,\"b\":49}", - "name": "calculate" - }, - "id": "call_dXQI5d9UAYuRygDqNMunj2W2", - "type": "function" - } - ] - }, - { - "content": "6223", - "role": "tool", - "tool_call_id": "call_dXQI5d9UAYuRygDqNMunj2W2" - } - ], - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": ["add", "subtract", "multiply", "divide"], - "type": "string" - } - }, - "required": ["operation", "a", "b"], - "type": "object" - } - }, - "type": "function" - } - ] - } - }, - "headers": {}, - "method": "POST", - "url": "https://api.openai.com/v1/chat/completions" - }, - "response": { - "body": { - "kind": "json", - "value": { - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "annotations": [], - "content": "127 multiplied by 49 is 6223.", - "refusal": null, - "role": "assistant" - } - } - ], - "created": 1787574027, - "id": "chatcmpl-EGNx5rDFm8HaHv7GCMw3OzQyu24L9", - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": "default", - "system_fingerprint": "fp_0950b7f384", - "usage": { - "completion_tokens": 11, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 106, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 117 - } - } - }, - "headers": { - "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", - "alt-svc": "h3=\":443\"; ma=86400", - "cf-cache-status": "DYNAMIC", - "cf-ray": "a3024fa7be702fd0-IAD", - "connection": "keep-alive", - "content-encoding": "gzip", - "content-type": "application/json", - "date": "Mon, 24 Aug 2026 12:20:28 GMT", - "openai-organization": "braintrust-data", - "openai-processing-ms": "592", - "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", - "openai-version": "2020-10-01", - "server": "cloudflare", - "set-cookie": "[REDACTED]", - "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "transfer-encoding": "chunked", - "x-content-type-options": "nosniff", - "x-openai-proxy-wasm": "v0.1", - "x-ratelimit-limit-requests": "30000", - "x-ratelimit-limit-tokens": "150000000", - "x-ratelimit-remaining-requests": "29999", - "x-ratelimit-remaining-tokens": "149999980", - "x-ratelimit-reset-requests": "2ms", - "x-ratelimit-reset-tokens": "0s", - "x-request-id": "req_80ec4ce95cf14b989fddb3c82140dab8" - }, - "status": 200, - "statusText": "OK" - } - } - ], - "meta": { - "createdAt": "2026-07-06T14:01:28.206Z" - } -} diff --git a/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1-latest.span-tree.json b/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1-latest.span-tree.json deleted file mode 100644 index 9d47ae91c..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1-latest.span-tree.json +++ /dev/null @@ -1,1511 +0,0 @@ -{ - "span_tree": [ - { - "name": "langchain-wrapper-root", - "type": "task", - "children": [ - { - "name": "langchain-invoke-operation", - "children": [ - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Reply with exactly OK.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "stop" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "OK", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-5-nano-2025-08-07", - "model_provider": "openai", - "tokenUsage": { - "completionTokens": 266, - "promptTokens": 11, - "totalTokens": 277 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 11, - "output_token_details": { - "audio": 0, - "reasoning": 256 - }, - "output_tokens": 266, - "total_tokens": 277 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "OK" - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 266, - "promptTokens": 11, - "totalTokens": 277 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_completion_tokens": 512, - "model": "gpt-5-nano", - "stream": false - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_model_name": "gpt-5-nano", - "ls_model_type": "chat", - "ls_provider": "openai", - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.5.3" - } - }, - "model": "gpt-5-nano-2025-08-07", - "options": {}, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 512, - "model": "gpt-5-nano", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - } - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 256, - "completion_tokens": 266, - "prompt_cached_tokens": 0, - "prompt_tokens": 11, - "tokens": 277 - } - } - ], - "metadata": { - "operation": "invoke", - "testRunId": "" - } - }, - { - "name": "langchain-chain-operation", - "children": [ - { - "name": "RunnableSequence", - "type": "task", - "children": [ - { - "name": "ChatPromptTemplate", - "type": "task", - "children": [], - "input": { - "word": "PARIS" - }, - "output": { - "id": [ - "langchain_core", - "prompt_values", - "ChatPromptValue" - ], - "kwargs": { - "messages": [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Reply with the single word PARIS and nothing else.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - "metadata": { - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "metadata": {}, - "name": "ChatPromptTemplate", - "parent_run_id": "", - "run_id": "", - "run_type": "prompt", - "serialized": { - "id": [ - "langchain_core", - "prompts", - "chat", - "ChatPromptTemplate" - ], - "kwargs": { - "input_variables": [ - "word" - ], - "messages": [ - { - "id": [ - "langchain_core", - "prompts", - "chat", - "HumanMessagePromptTemplate" - ], - "kwargs": { - "prompt": { - "id": [ - "langchain_core", - "prompts", - "prompt", - "PromptTemplate" - ], - "kwargs": { - "input_variables": [ - "word" - ], - "template": "Reply with the single word {word} and nothing else.", - "template_format": "f-string" - }, - "lc": 1, - "type": "constructor" - } - }, - "lc": 1, - "type": "constructor" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - "tags": [ - "seq:step:1" - ] - } - }, - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Reply with the single word PARIS and nothing else.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "stop" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "PARIS", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "usage": { - "completion_tokens": 2, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 18, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 20 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 18, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 2, - "total_tokens": 20 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "PARIS" - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0 - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 32, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.5.3" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": {}, - "parent_run_id": "", - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [ - "seq:step:2" - ] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 2, - "prompt_cached_tokens": 0, - "prompt_tokens": 18, - "tokens": 20 - } - } - ], - "input": { - "word": "PARIS" - }, - "output": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "PARIS", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "usage": { - "completion_tokens": 2, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 18, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 20 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 18, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 2, - "total_tokens": 20 - } - }, - "lc": 1, - "type": "constructor" - }, - "metadata": { - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "metadata": {}, - "run_id": "", - "serialized": { - "id": [ - "langchain_core", - "runnables", - "RunnableSequence" - ], - "kwargs": { - "first": { - "id": [ - "langchain_core", - "prompts", - "chat", - "ChatPromptTemplate" - ], - "kwargs": { - "input_variables": [ - "word" - ], - "messages": [ - { - "id": [ - "langchain_core", - "prompts", - "chat", - "HumanMessagePromptTemplate" - ], - "kwargs": { - "prompt": { - "id": [ - "langchain_core", - "prompts", - "prompt", - "PromptTemplate" - ], - "kwargs": { - "input_variables": [ - "word" - ], - "template": "Reply with the single word {word} and nothing else.", - "template_format": "f-string" - }, - "lc": 1, - "type": "constructor" - } - }, - "lc": 1, - "type": "constructor" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - "last": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - } - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - } - } - ], - "metadata": { - "operation": "chain", - "testRunId": "" - } - }, - { - "name": "langchain-stream-operation", - "children": [ - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Count from 1 to 3 and include the words one two three.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "completion": 0, - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "prompt": 0, - "service_tier": "default", - "system_fingerprint": "" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessageChunk" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "One, two, three.", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "completion": 0, - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "prompt": 0, - "service_tier": "default", - "system_fingerprint": "", - "usage": { - "completion_tokens": 6, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 22, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 28 - } - }, - "tool_call_chunks": [], - "tool_calls": [], - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 22, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 6, - "total_tokens": 28 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "One, two, three." - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 6, - "promptTokens": 22, - "totalTokens": 28 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "stream": true, - "stream_options": { - "include_usage": true - }, - "temperature": 0 - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 32, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.5.3" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": {}, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "streaming": true, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 6, - "prompt_cached_tokens": 0, - "prompt_tokens": 22, - "time_to_first_token": 0, - "tokens": 28 - } - } - ], - "metadata": { - "operation": "stream", - "testRunId": "" - } - }, - { - "name": "langchain-tool-operation", - "children": [ - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Use the get_weather tool for Paris, France. Do not answer from memory.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "tool_calls" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": { - "tool_calls": [ - { - "function": { - "arguments": "{\"location\":\"Paris, France\"}", - "name": "get_weather" - }, - "id": "", - "type": "function" - } - ] - }, - "content": "", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "tool_calls", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 16, - "promptTokens": 72, - "totalTokens": 88 - }, - "usage": { - "completion_tokens": 16, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 72, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 88 - } - }, - "tool_calls": [ - { - "args": { - "location": "Paris, France" - }, - "id": "", - "name": "get_weather", - "type": "tool_call" - } - ], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 72, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 16, - "total_tokens": 88 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "" - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 16, - "promptTokens": 72, - "totalTokens": 88 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Get the current weather in a given location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and state or city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 128, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.5.3" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": { - "tools": [ - { - "function": { - "description": "Get the current weather in a given location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and state or city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 16, - "prompt_cached_tokens": 0, - "prompt_tokens": 72, - "tokens": 88 - } - } - ], - "metadata": { - "operation": "tool", - "testRunId": "" - } - }, - { - "name": "langchain-tool-result-operation", - "children": [ - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "tool_calls" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": { - "tool_calls": [ - { - "function": { - "arguments": "{\"operation\":\"multiply\",\"a\":127,\"b\":49}", - "name": "calculate" - }, - "id": "", - "type": "function" - } - ] - }, - "content": "", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "tool_calls", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 21, - "promptTokens": 76, - "totalTokens": 97 - }, - "usage": { - "completion_tokens": 21, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 76, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 97 - } - }, - "tool_calls": [ - { - "args": { - "a": 127, - "b": 49, - "operation": "multiply" - }, - "id": "", - "name": "calculate", - "type": "tool_call" - } - ], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 76, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 21, - "total_tokens": 97 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "" - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 21, - "promptTokens": 76, - "totalTokens": 97 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 128, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.5.3" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": { - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 21, - "prompt_cached_tokens": 0, - "prompt_tokens": 76, - "tokens": 97 - } - }, - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - }, - { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "", - "invalid_tool_calls": [], - "response_metadata": {}, - "tool_calls": [ - { - "args": { - "a": 127, - "b": 49, - "operation": "multiply" - }, - "id": "", - "name": "calculate", - "type": "tool_call" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - { - "id": [ - "langchain_core", - "messages", - "ToolMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "6223", - "response_metadata": {}, - "tool_call_id": "call_BMkQ0blzGQeVPEC7pIElPdRs" - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "stop" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "127 multiplied by 49 is 6223.", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 11, - "promptTokens": 106, - "totalTokens": 117 - }, - "usage": { - "completion_tokens": 11, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 106, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 117 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 106, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 11, - "total_tokens": 117 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "127 multiplied by 49 is 6223." - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 11, - "promptTokens": 106, - "totalTokens": 117 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 128, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.5.3" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": { - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 11, - "prompt_cached_tokens": 0, - "prompt_tokens": 106, - "tokens": 117 - } - } - ], - "metadata": { - "operation": "tool-result", - "testRunId": "" - } - } - ], - "metadata": { - "scenario": "wrap-langchain-js-traces", - "testRunId": "" - } - } - ] -} diff --git a/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1-latest.span-tree.txt b/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1-latest.span-tree.txt deleted file mode 100644 index d19c42372..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1-latest.span-tree.txt +++ /dev/null @@ -1,1450 +0,0 @@ -span_tree: -└── langchain-wrapper-root [task] - metadata: { - "scenario": "wrap-langchain-js-traces", - "testRunId": "" - } - ├── langchain-invoke-operation - │ metadata: { - │ "operation": "invoke", - │ "testRunId": "" - │ } - │ └── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "Reply with exactly OK.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "finish_reason": "stop" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "OK", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "stop", - │ "model_name": "gpt-5-nano-2025-08-07", - │ "model_provider": "openai", - │ "tokenUsage": { - │ "completionTokens": 266, - │ "promptTokens": 11, - │ "totalTokens": 277 - │ } - │ }, - │ "tool_calls": [], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 11, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 256 - │ }, - │ "output_tokens": 266, - │ "total_tokens": 277 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "OK" - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 266, - │ "promptTokens": 11, - │ "totalTokens": 277 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_completion_tokens": 512, - │ "model": "gpt-5-nano", - │ "stream": false - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_model_name": "gpt-5-nano", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.5.3" - │ } - │ }, - │ "model": "gpt-5-nano-2025-08-07", - │ "options": {}, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 512, - │ "model": "gpt-5-nano", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 256, - │ "completion_tokens": 266, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 11, - │ "tokens": 277 - │ } - ├── langchain-chain-operation - │ metadata: { - │ "operation": "chain", - │ "testRunId": "" - │ } - │ └── RunnableSequence [task] - │ input: { - │ "word": "PARIS" - │ } - │ output: { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "PARIS", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "stop", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "system_fingerprint": "", - │ "tokenUsage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "usage": { - │ "completion_tokens": 2, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 18, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 20 - │ } - │ }, - │ "tool_calls": [], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 18, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 2, - │ "total_tokens": 20 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "metadata": {}, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain_core", - │ "runnables", - │ "RunnableSequence" - │ ], - │ "kwargs": { - │ "first": { - │ "id": [ - │ "langchain_core", - │ "prompts", - │ "chat", - │ "ChatPromptTemplate" - │ ], - │ "kwargs": { - │ "input_variables": [ - │ "word" - │ ], - │ "messages": [ - │ { - │ "id": [ - │ "langchain_core", - │ "prompts", - │ "chat", - │ "HumanMessagePromptTemplate" - │ ], - │ "kwargs": { - │ "prompt": { - │ "id": [ - │ "langchain_core", - │ "prompts", - │ "prompt", - │ "PromptTemplate" - │ ], - │ "kwargs": { - │ "input_variables": [ - │ "word" - │ ], - │ "template": "Reply with the single word {word} and nothing else.", - │ "template_format": "f-string" - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "last": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ ├── ChatPromptTemplate [task] - │ │ input: { - │ │ "word": "PARIS" - │ │ } - │ │ output: { - │ │ "id": [ - │ │ "langchain_core", - │ │ "prompt_values", - │ │ "ChatPromptValue" - │ │ ], - │ │ "kwargs": { - │ │ "messages": [ - │ │ { - │ │ "id": [ - │ │ "langchain_core", - │ │ "messages", - │ │ "HumanMessage" - │ │ ], - │ │ "kwargs": { - │ │ "additional_kwargs": {}, - │ │ "content": "Reply with the single word PARIS and nothing else.", - │ │ "response_metadata": {} - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ } - │ │ ] - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ } - │ │ metadata: { - │ │ "braintrust": { - │ │ "integration_name": "langchain-js", - │ │ "sdk_language": "javascript" - │ │ }, - │ │ "metadata": {}, - │ │ "name": "ChatPromptTemplate", - │ │ "parent_run_id": "", - │ │ "run_id": "", - │ │ "run_type": "prompt", - │ │ "serialized": { - │ │ "id": [ - │ │ "langchain_core", - │ │ "prompts", - │ │ "chat", - │ │ "ChatPromptTemplate" - │ │ ], - │ │ "kwargs": { - │ │ "input_variables": [ - │ │ "word" - │ │ ], - │ │ "messages": [ - │ │ { - │ │ "id": [ - │ │ "langchain_core", - │ │ "prompts", - │ │ "chat", - │ │ "HumanMessagePromptTemplate" - │ │ ], - │ │ "kwargs": { - │ │ "prompt": { - │ │ "id": [ - │ │ "langchain_core", - │ │ "prompts", - │ │ "prompt", - │ │ "PromptTemplate" - │ │ ], - │ │ "kwargs": { - │ │ "input_variables": [ - │ │ "word" - │ │ ], - │ │ "template": "Reply with the single word {word} and nothing else.", - │ │ "template_format": "f-string" - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ } - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ } - │ │ ] - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ }, - │ │ "tags": [ - │ │ "seq:step:1" - │ │ ] - │ │ } - │ └── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "Reply with the single word PARIS and nothing else.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "finish_reason": "stop" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "PARIS", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "stop", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "system_fingerprint": "", - │ "tokenUsage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "usage": { - │ "completion_tokens": 2, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 18, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 20 - │ } - │ }, - │ "tool_calls": [], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 18, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 2, - │ "total_tokens": 20 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "PARIS" - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "stream": false, - │ "temperature": 0 - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_max_tokens": 32, - │ "ls_model_name": "gpt-4o-mini-2024-07-18", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "ls_temperature": 0, - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.5.3" - │ } - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "options": {}, - │ "parent_run_id": "", - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [ - │ "seq:step:2" - │ ] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 0, - │ "completion_tokens": 2, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 18, - │ "tokens": 20 - │ } - ├── langchain-stream-operation - │ metadata: { - │ "operation": "stream", - │ "testRunId": "" - │ } - │ └── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "Count from 1 to 3 and include the words one two three.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "completion": 0, - │ "finish_reason": "stop", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "prompt": 0, - │ "service_tier": "default", - │ "system_fingerprint": "" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessageChunk" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "One, two, three.", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "completion": 0, - │ "finish_reason": "stop", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "prompt": 0, - │ "service_tier": "default", - │ "system_fingerprint": "", - │ "usage": { - │ "completion_tokens": 6, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 22, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 28 - │ } - │ }, - │ "tool_call_chunks": [], - │ "tool_calls": [], - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 22, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 6, - │ "total_tokens": 28 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "One, two, three." - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 6, - │ "promptTokens": 22, - │ "totalTokens": 28 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "stream": true, - │ "stream_options": { - │ "include_usage": true - │ }, - │ "temperature": 0 - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_max_tokens": 32, - │ "ls_model_name": "gpt-4o-mini-2024-07-18", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "ls_temperature": 0, - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.5.3" - │ } - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "options": {}, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "streaming": true, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 0, - │ "completion_tokens": 6, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 22, - │ "time_to_first_token": 0, - │ "tokens": 28 - │ } - ├── langchain-tool-operation - │ metadata: { - │ "operation": "tool", - │ "testRunId": "" - │ } - │ └── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "Use the get_weather tool for Paris, France. Do not answer from memory.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "finish_reason": "tool_calls" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": { - │ "tool_calls": [ - │ { - │ "function": { - │ "arguments": "{\"location\":\"Paris, France\"}", - │ "name": "get_weather" - │ }, - │ "id": "", - │ "type": "function" - │ } - │ ] - │ }, - │ "content": "", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "tool_calls", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "system_fingerprint": "", - │ "tokenUsage": { - │ "completionTokens": 16, - │ "promptTokens": 72, - │ "totalTokens": 88 - │ }, - │ "usage": { - │ "completion_tokens": 16, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 72, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 88 - │ } - │ }, - │ "tool_calls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "id": "", - │ "name": "get_weather", - │ "type": "tool_call" - │ } - │ ], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 72, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 16, - │ "total_tokens": 88 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "" - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 16, - │ "promptTokens": 72, - │ "totalTokens": 88 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_tokens": 128, - │ "model": "gpt-4o-mini-2024-07-18", - │ "stream": false, - │ "temperature": 0, - │ "tools": [ - │ { - │ "function": { - │ "description": "Get the current weather in a given location", - │ "name": "get_weather", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and state or city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ }, - │ "type": "function" - │ } - │ ] - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_max_tokens": 128, - │ "ls_model_name": "gpt-4o-mini-2024-07-18", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "ls_temperature": 0, - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.5.3" - │ } - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "options": { - │ "tools": [ - │ { - │ "function": { - │ "description": "Get the current weather in a given location", - │ "name": "get_weather", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and state or city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ }, - │ "type": "function" - │ } - │ ] - │ }, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 128, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 0, - │ "completion_tokens": 16, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 72, - │ "tokens": 88 - │ } - └── langchain-tool-result-operation - metadata: { - "operation": "tool-result", - "testRunId": "" - } - ├── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "What is 127 multiplied by 49? Use the calculate tool.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "finish_reason": "tool_calls" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": { - │ "tool_calls": [ - │ { - │ "function": { - │ "arguments": "{\"operation\":\"multiply\",\"a\":127,\"b\":49}", - │ "name": "calculate" - │ }, - │ "id": "", - │ "type": "function" - │ } - │ ] - │ }, - │ "content": "", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "tool_calls", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "system_fingerprint": "", - │ "tokenUsage": { - │ "completionTokens": 21, - │ "promptTokens": 76, - │ "totalTokens": 97 - │ }, - │ "usage": { - │ "completion_tokens": 21, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 76, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 97 - │ } - │ }, - │ "tool_calls": [ - │ { - │ "args": { - │ "a": 127, - │ "b": 49, - │ "operation": "multiply" - │ }, - │ "id": "", - │ "name": "calculate", - │ "type": "tool_call" - │ } - │ ], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 76, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 21, - │ "total_tokens": 97 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "" - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 21, - │ "promptTokens": 76, - │ "totalTokens": 97 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_tokens": 128, - │ "model": "gpt-4o-mini-2024-07-18", - │ "stream": false, - │ "temperature": 0, - │ "tools": [ - │ { - │ "function": { - │ "description": "Perform a mathematical calculation", - │ "name": "calculate", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "a": { - │ "type": "number" - │ }, - │ "b": { - │ "type": "number" - │ }, - │ "operation": { - │ "enum": [ - │ "add", - │ "subtract", - │ "multiply", - │ "divide" - │ ], - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "operation", - │ "a", - │ "b" - │ ], - │ "type": "object" - │ } - │ }, - │ "type": "function" - │ } - │ ] - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_max_tokens": 128, - │ "ls_model_name": "gpt-4o-mini-2024-07-18", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "ls_temperature": 0, - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.5.3" - │ } - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "options": { - │ "tools": [ - │ { - │ "function": { - │ "description": "Perform a mathematical calculation", - │ "name": "calculate", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "a": { - │ "type": "number" - │ }, - │ "b": { - │ "type": "number" - │ }, - │ "operation": { - │ "enum": [ - │ "add", - │ "subtract", - │ "multiply", - │ "divide" - │ ], - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "operation", - │ "a", - │ "b" - │ ], - │ "type": "object" - │ } - │ }, - │ "type": "function" - │ } - │ ] - │ }, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 128, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 0, - │ "completion_tokens": 21, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 76, - │ "tokens": 97 - │ } - └── ChatOpenAI [llm] - input: [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - }, - { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "", - "invalid_tool_calls": [], - "response_metadata": {}, - "tool_calls": [ - { - "args": { - "a": 127, - "b": 49, - "operation": "multiply" - }, - "id": "", - "name": "calculate", - "type": "tool_call" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - { - "id": [ - "langchain_core", - "messages", - "ToolMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "6223", - "response_metadata": {}, - "tool_call_id": "call_BMkQ0blzGQeVPEC7pIElPdRs" - }, - "lc": 1, - "type": "constructor" - } - ] - ] - output: { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "stop" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "127 multiplied by 49 is 6223.", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 11, - "promptTokens": 106, - "totalTokens": 117 - }, - "usage": { - "completion_tokens": 11, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 106, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 117 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 106, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 11, - "total_tokens": 117 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "127 multiplied by 49 is 6223." - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 11, - "promptTokens": 106, - "totalTokens": 117 - } - } - } - metadata: { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 128, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.5.3" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": { - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - } - metrics: { - "completion_reasoning_tokens": 0, - "completion_tokens": 11, - "prompt_cached_tokens": 0, - "prompt_tokens": 106, - "tokens": 117 - } diff --git a/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1.span-tree.json b/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1.span-tree.json deleted file mode 100644 index f9106045f..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1.span-tree.json +++ /dev/null @@ -1,1511 +0,0 @@ -{ - "span_tree": [ - { - "name": "langchain-wrapper-root", - "type": "task", - "children": [ - { - "name": "langchain-invoke-operation", - "children": [ - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Reply with exactly OK.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "stop" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "OK", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-5-nano-2025-08-07", - "model_provider": "openai", - "tokenUsage": { - "completionTokens": 138, - "promptTokens": 11, - "totalTokens": 149 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 11, - "output_token_details": { - "audio": 0, - "reasoning": 128 - }, - "output_tokens": 138, - "total_tokens": 149 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "OK" - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 138, - "promptTokens": 11, - "totalTokens": 149 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_completion_tokens": 512, - "model": "gpt-5-nano", - "stream": false - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_model_name": "gpt-5-nano", - "ls_model_type": "chat", - "ls_provider": "openai", - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.3.0" - } - }, - "model": "gpt-5-nano-2025-08-07", - "options": {}, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 512, - "model": "gpt-5-nano", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - } - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 128, - "completion_tokens": 138, - "prompt_cached_tokens": 0, - "prompt_tokens": 11, - "tokens": 149 - } - } - ], - "metadata": { - "operation": "invoke", - "testRunId": "" - } - }, - { - "name": "langchain-chain-operation", - "children": [ - { - "name": "RunnableSequence", - "type": "task", - "children": [ - { - "name": "ChatPromptTemplate", - "type": "task", - "children": [], - "input": { - "word": "PARIS" - }, - "output": { - "id": [ - "langchain_core", - "prompt_values", - "ChatPromptValue" - ], - "kwargs": { - "messages": [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Reply with the single word PARIS and nothing else.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - "metadata": { - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "metadata": {}, - "name": "ChatPromptTemplate", - "parent_run_id": "", - "run_id": "", - "run_type": "prompt", - "serialized": { - "id": [ - "langchain_core", - "prompts", - "chat", - "ChatPromptTemplate" - ], - "kwargs": { - "input_variables": [ - "word" - ], - "messages": [ - { - "id": [ - "langchain_core", - "prompts", - "chat", - "HumanMessagePromptTemplate" - ], - "kwargs": { - "prompt": { - "id": [ - "langchain_core", - "prompts", - "prompt", - "PromptTemplate" - ], - "kwargs": { - "input_variables": [ - "word" - ], - "template": "Reply with the single word {word} and nothing else.", - "template_format": "f-string" - }, - "lc": 1, - "type": "constructor" - } - }, - "lc": 1, - "type": "constructor" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - "tags": [ - "seq:step:1" - ] - } - }, - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Reply with the single word PARIS and nothing else.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "stop" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "PARIS", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "usage": { - "completion_tokens": 2, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 18, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 20 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 18, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 2, - "total_tokens": 20 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "PARIS" - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0 - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 32, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.3.0" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": {}, - "parent_run_id": "", - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [ - "seq:step:2" - ] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 2, - "prompt_cached_tokens": 0, - "prompt_tokens": 18, - "tokens": 20 - } - } - ], - "input": { - "word": "PARIS" - }, - "output": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "PARIS", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 2, - "promptTokens": 18, - "totalTokens": 20 - }, - "usage": { - "completion_tokens": 2, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 18, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 20 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 18, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 2, - "total_tokens": 20 - } - }, - "lc": 1, - "type": "constructor" - }, - "metadata": { - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "metadata": {}, - "run_id": "", - "serialized": { - "id": [ - "langchain_core", - "runnables", - "RunnableSequence" - ], - "kwargs": { - "first": { - "id": [ - "langchain_core", - "prompts", - "chat", - "ChatPromptTemplate" - ], - "kwargs": { - "input_variables": [ - "word" - ], - "messages": [ - { - "id": [ - "langchain_core", - "prompts", - "chat", - "HumanMessagePromptTemplate" - ], - "kwargs": { - "prompt": { - "id": [ - "langchain_core", - "prompts", - "prompt", - "PromptTemplate" - ], - "kwargs": { - "input_variables": [ - "word" - ], - "template": "Reply with the single word {word} and nothing else.", - "template_format": "f-string" - }, - "lc": 1, - "type": "constructor" - } - }, - "lc": 1, - "type": "constructor" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - "last": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - } - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - } - } - ], - "metadata": { - "operation": "chain", - "testRunId": "" - } - }, - { - "name": "langchain-stream-operation", - "children": [ - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Count from 1 to 3 and include the words one two three.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "completion": 0, - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "prompt": 0, - "service_tier": "default", - "system_fingerprint": "" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessageChunk" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "One, two, three.", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "completion": 0, - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "prompt": 0, - "service_tier": "default", - "system_fingerprint": "", - "usage": { - "completion_tokens": 6, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 22, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 28 - } - }, - "tool_call_chunks": [], - "tool_calls": [], - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 22, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 6, - "total_tokens": 28 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "One, two, three." - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 6, - "promptTokens": 22, - "totalTokens": 28 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "stream": true, - "stream_options": { - "include_usage": true - }, - "temperature": 0 - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 32, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.3.0" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": {}, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 32, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "streaming": true, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 6, - "prompt_cached_tokens": 0, - "prompt_tokens": 22, - "time_to_first_token": 0, - "tokens": 28 - } - } - ], - "metadata": { - "operation": "stream", - "testRunId": "" - } - }, - { - "name": "langchain-tool-operation", - "children": [ - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "Use the get_weather tool for Paris, France. Do not answer from memory.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "tool_calls" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": { - "tool_calls": [ - { - "function": { - "arguments": "{\"location\":\"Paris, France\"}", - "name": "get_weather" - }, - "id": "", - "type": "function" - } - ] - }, - "content": "", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "tool_calls", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 16, - "promptTokens": 72, - "totalTokens": 88 - }, - "usage": { - "completion_tokens": 16, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 72, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 88 - } - }, - "tool_calls": [ - { - "args": { - "location": "Paris, France" - }, - "id": "", - "name": "get_weather", - "type": "tool_call" - } - ], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 72, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 16, - "total_tokens": 88 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "" - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 16, - "promptTokens": 72, - "totalTokens": 88 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Get the current weather in a given location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and state or city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 128, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.3.0" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": { - "tools": [ - { - "function": { - "description": "Get the current weather in a given location", - "name": "get_weather", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "location": { - "description": "The city and state or city and country", - "type": "string" - } - }, - "required": [ - "location" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 16, - "prompt_cached_tokens": 0, - "prompt_tokens": 72, - "tokens": 88 - } - } - ], - "metadata": { - "operation": "tool", - "testRunId": "" - } - }, - { - "name": "langchain-tool-result-operation", - "children": [ - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "tool_calls" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": { - "tool_calls": [ - { - "function": { - "arguments": "{\"operation\":\"multiply\",\"a\":127,\"b\":49}", - "name": "calculate" - }, - "id": "", - "type": "function" - } - ] - }, - "content": "", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "tool_calls", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 21, - "promptTokens": 76, - "totalTokens": 97 - }, - "usage": { - "completion_tokens": 21, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 76, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 97 - } - }, - "tool_calls": [ - { - "args": { - "a": 127, - "b": 49, - "operation": "multiply" - }, - "id": "", - "name": "calculate", - "type": "tool_call" - } - ], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 76, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 21, - "total_tokens": 97 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "" - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 21, - "promptTokens": 76, - "totalTokens": 97 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 128, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.3.0" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": { - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 21, - "prompt_cached_tokens": 0, - "prompt_tokens": 76, - "tokens": 97 - } - }, - { - "name": "ChatOpenAI", - "type": "llm", - "children": [], - "input": [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - }, - { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "", - "invalid_tool_calls": [], - "response_metadata": {}, - "tool_calls": [ - { - "args": { - "a": 127, - "b": 49, - "operation": "multiply" - }, - "id": "", - "name": "calculate", - "type": "tool_call" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - { - "id": [ - "langchain_core", - "messages", - "ToolMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "6223", - "response_metadata": {}, - "tool_call_id": "call_dXQI5d9UAYuRygDqNMunj2W2" - }, - "lc": 1, - "type": "constructor" - } - ] - ], - "output": { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "stop" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "127 multiplied by 49 is 6223.", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 11, - "promptTokens": 106, - "totalTokens": 117 - }, - "usage": { - "completion_tokens": 11, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 106, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 117 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 106, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 11, - "total_tokens": 117 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "127 multiplied by 49 is 6223." - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 11, - "promptTokens": 106, - "totalTokens": 117 - } - } - }, - "metadata": { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 128, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.3.0" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": { - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - }, - "metrics": { - "completion_reasoning_tokens": 0, - "completion_tokens": 11, - "prompt_cached_tokens": 0, - "prompt_tokens": 106, - "tokens": 117 - } - } - ], - "metadata": { - "operation": "tool-result", - "testRunId": "" - } - } - ], - "metadata": { - "scenario": "wrap-langchain-js-traces", - "testRunId": "" - } - } - ] -} diff --git a/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1.span-tree.txt b/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1.span-tree.txt deleted file mode 100644 index 7677c0bdd..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/__snapshots__/wrap-langchain-js-traces-v1.span-tree.txt +++ /dev/null @@ -1,1450 +0,0 @@ -span_tree: -└── langchain-wrapper-root [task] - metadata: { - "scenario": "wrap-langchain-js-traces", - "testRunId": "" - } - ├── langchain-invoke-operation - │ metadata: { - │ "operation": "invoke", - │ "testRunId": "" - │ } - │ └── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "Reply with exactly OK.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "finish_reason": "stop" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "OK", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "stop", - │ "model_name": "gpt-5-nano-2025-08-07", - │ "model_provider": "openai", - │ "tokenUsage": { - │ "completionTokens": 138, - │ "promptTokens": 11, - │ "totalTokens": 149 - │ } - │ }, - │ "tool_calls": [], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 11, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 128 - │ }, - │ "output_tokens": 138, - │ "total_tokens": 149 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "OK" - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 138, - │ "promptTokens": 11, - │ "totalTokens": 149 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_completion_tokens": 512, - │ "model": "gpt-5-nano", - │ "stream": false - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_model_name": "gpt-5-nano", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.3.0" - │ } - │ }, - │ "model": "gpt-5-nano-2025-08-07", - │ "options": {}, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 512, - │ "model": "gpt-5-nano", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 128, - │ "completion_tokens": 138, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 11, - │ "tokens": 149 - │ } - ├── langchain-chain-operation - │ metadata: { - │ "operation": "chain", - │ "testRunId": "" - │ } - │ └── RunnableSequence [task] - │ input: { - │ "word": "PARIS" - │ } - │ output: { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "PARIS", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "stop", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "system_fingerprint": "", - │ "tokenUsage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "usage": { - │ "completion_tokens": 2, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 18, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 20 - │ } - │ }, - │ "tool_calls": [], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 18, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 2, - │ "total_tokens": 20 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ metadata: { - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "metadata": {}, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain_core", - │ "runnables", - │ "RunnableSequence" - │ ], - │ "kwargs": { - │ "first": { - │ "id": [ - │ "langchain_core", - │ "prompts", - │ "chat", - │ "ChatPromptTemplate" - │ ], - │ "kwargs": { - │ "input_variables": [ - │ "word" - │ ], - │ "messages": [ - │ { - │ "id": [ - │ "langchain_core", - │ "prompts", - │ "chat", - │ "HumanMessagePromptTemplate" - │ ], - │ "kwargs": { - │ "prompt": { - │ "id": [ - │ "langchain_core", - │ "prompts", - │ "prompt", - │ "PromptTemplate" - │ ], - │ "kwargs": { - │ "input_variables": [ - │ "word" - │ ], - │ "template": "Reply with the single word {word} and nothing else.", - │ "template_format": "f-string" - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "last": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ ├── ChatPromptTemplate [task] - │ │ input: { - │ │ "word": "PARIS" - │ │ } - │ │ output: { - │ │ "id": [ - │ │ "langchain_core", - │ │ "prompt_values", - │ │ "ChatPromptValue" - │ │ ], - │ │ "kwargs": { - │ │ "messages": [ - │ │ { - │ │ "id": [ - │ │ "langchain_core", - │ │ "messages", - │ │ "HumanMessage" - │ │ ], - │ │ "kwargs": { - │ │ "additional_kwargs": {}, - │ │ "content": "Reply with the single word PARIS and nothing else.", - │ │ "response_metadata": {} - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ } - │ │ ] - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ } - │ │ metadata: { - │ │ "braintrust": { - │ │ "integration_name": "langchain-js", - │ │ "sdk_language": "javascript" - │ │ }, - │ │ "metadata": {}, - │ │ "name": "ChatPromptTemplate", - │ │ "parent_run_id": "", - │ │ "run_id": "", - │ │ "run_type": "prompt", - │ │ "serialized": { - │ │ "id": [ - │ │ "langchain_core", - │ │ "prompts", - │ │ "chat", - │ │ "ChatPromptTemplate" - │ │ ], - │ │ "kwargs": { - │ │ "input_variables": [ - │ │ "word" - │ │ ], - │ │ "messages": [ - │ │ { - │ │ "id": [ - │ │ "langchain_core", - │ │ "prompts", - │ │ "chat", - │ │ "HumanMessagePromptTemplate" - │ │ ], - │ │ "kwargs": { - │ │ "prompt": { - │ │ "id": [ - │ │ "langchain_core", - │ │ "prompts", - │ │ "prompt", - │ │ "PromptTemplate" - │ │ ], - │ │ "kwargs": { - │ │ "input_variables": [ - │ │ "word" - │ │ ], - │ │ "template": "Reply with the single word {word} and nothing else.", - │ │ "template_format": "f-string" - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ } - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ } - │ │ ] - │ │ }, - │ │ "lc": 1, - │ │ "type": "constructor" - │ │ }, - │ │ "tags": [ - │ │ "seq:step:1" - │ │ ] - │ │ } - │ └── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "Reply with the single word PARIS and nothing else.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "finish_reason": "stop" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "PARIS", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "stop", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "system_fingerprint": "", - │ "tokenUsage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ }, - │ "usage": { - │ "completion_tokens": 2, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 18, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 20 - │ } - │ }, - │ "tool_calls": [], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 18, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 2, - │ "total_tokens": 20 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "PARIS" - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 2, - │ "promptTokens": 18, - │ "totalTokens": 20 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "stream": false, - │ "temperature": 0 - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_max_tokens": 32, - │ "ls_model_name": "gpt-4o-mini-2024-07-18", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "ls_temperature": 0, - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.3.0" - │ } - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "options": {}, - │ "parent_run_id": "", - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [ - │ "seq:step:2" - │ ] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 0, - │ "completion_tokens": 2, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 18, - │ "tokens": 20 - │ } - ├── langchain-stream-operation - │ metadata: { - │ "operation": "stream", - │ "testRunId": "" - │ } - │ └── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "Count from 1 to 3 and include the words one two three.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "completion": 0, - │ "finish_reason": "stop", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "prompt": 0, - │ "service_tier": "default", - │ "system_fingerprint": "" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessageChunk" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "One, two, three.", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "completion": 0, - │ "finish_reason": "stop", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "prompt": 0, - │ "service_tier": "default", - │ "system_fingerprint": "", - │ "usage": { - │ "completion_tokens": 6, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 22, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 28 - │ } - │ }, - │ "tool_call_chunks": [], - │ "tool_calls": [], - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 22, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 6, - │ "total_tokens": 28 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "One, two, three." - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 6, - │ "promptTokens": 22, - │ "totalTokens": 28 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "stream": true, - │ "stream_options": { - │ "include_usage": true - │ }, - │ "temperature": 0 - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_max_tokens": 32, - │ "ls_model_name": "gpt-4o-mini-2024-07-18", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "ls_temperature": 0, - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.3.0" - │ } - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "options": {}, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 32, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "streaming": true, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 0, - │ "completion_tokens": 6, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 22, - │ "time_to_first_token": 0, - │ "tokens": 28 - │ } - ├── langchain-tool-operation - │ metadata: { - │ "operation": "tool", - │ "testRunId": "" - │ } - │ └── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "Use the get_weather tool for Paris, France. Do not answer from memory.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "finish_reason": "tool_calls" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": { - │ "tool_calls": [ - │ { - │ "function": { - │ "arguments": "{\"location\":\"Paris, France\"}", - │ "name": "get_weather" - │ }, - │ "id": "", - │ "type": "function" - │ } - │ ] - │ }, - │ "content": "", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "tool_calls", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "system_fingerprint": "", - │ "tokenUsage": { - │ "completionTokens": 16, - │ "promptTokens": 72, - │ "totalTokens": 88 - │ }, - │ "usage": { - │ "completion_tokens": 16, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 72, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 88 - │ } - │ }, - │ "tool_calls": [ - │ { - │ "args": { - │ "location": "Paris, France" - │ }, - │ "id": "", - │ "name": "get_weather", - │ "type": "tool_call" - │ } - │ ], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 72, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 16, - │ "total_tokens": 88 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "" - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 16, - │ "promptTokens": 72, - │ "totalTokens": 88 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_tokens": 128, - │ "model": "gpt-4o-mini-2024-07-18", - │ "stream": false, - │ "temperature": 0, - │ "tools": [ - │ { - │ "function": { - │ "description": "Get the current weather in a given location", - │ "name": "get_weather", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and state or city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ }, - │ "type": "function" - │ } - │ ] - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_max_tokens": 128, - │ "ls_model_name": "gpt-4o-mini-2024-07-18", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "ls_temperature": 0, - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.3.0" - │ } - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "options": { - │ "tools": [ - │ { - │ "function": { - │ "description": "Get the current weather in a given location", - │ "name": "get_weather", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "location": { - │ "description": "The city and state or city and country", - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "location" - │ ], - │ "type": "object" - │ } - │ }, - │ "type": "function" - │ } - │ ] - │ }, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 128, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 0, - │ "completion_tokens": 16, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 72, - │ "tokens": 88 - │ } - └── langchain-tool-result-operation - metadata: { - "operation": "tool-result", - "testRunId": "" - } - ├── ChatOpenAI [llm] - │ input: [ - │ [ - │ { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "HumanMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": {}, - │ "content": "What is 127 multiplied by 49? Use the calculate tool.", - │ "response_metadata": {} - │ }, - │ "lc": 1, - │ "type": "constructor" - │ } - │ ] - │ ] - │ output: { - │ "generations": [ - │ [ - │ { - │ "generationInfo": { - │ "finish_reason": "tool_calls" - │ }, - │ "message": { - │ "id": [ - │ "langchain_core", - │ "messages", - │ "AIMessage" - │ ], - │ "kwargs": { - │ "additional_kwargs": { - │ "tool_calls": [ - │ { - │ "function": { - │ "arguments": "{\"operation\":\"multiply\",\"a\":127,\"b\":49}", - │ "name": "calculate" - │ }, - │ "id": "", - │ "type": "function" - │ } - │ ] - │ }, - │ "content": "", - │ "id": "", - │ "invalid_tool_calls": [], - │ "response_metadata": { - │ "finish_reason": "tool_calls", - │ "model_name": "gpt-4o-mini-2024-07-18", - │ "model_provider": "openai", - │ "system_fingerprint": "", - │ "tokenUsage": { - │ "completionTokens": 21, - │ "promptTokens": 76, - │ "totalTokens": 97 - │ }, - │ "usage": { - │ "completion_tokens": 21, - │ "completion_tokens_details": { - │ "accepted_prediction_tokens": 0, - │ "audio_tokens": 0, - │ "reasoning_tokens": 0, - │ "rejected_prediction_tokens": 0 - │ }, - │ "prompt_tokens": 76, - │ "prompt_tokens_details": { - │ "audio_tokens": 0, - │ "cached_tokens": 0 - │ }, - │ "total_tokens": 97 - │ } - │ }, - │ "tool_calls": [ - │ { - │ "args": { - │ "a": 127, - │ "b": 49, - │ "operation": "multiply" - │ }, - │ "id": "", - │ "name": "calculate", - │ "type": "tool_call" - │ } - │ ], - │ "type": "ai", - │ "usage_metadata": { - │ "input_token_details": { - │ "audio": 0, - │ "cache_read": 0 - │ }, - │ "input_tokens": 76, - │ "output_token_details": { - │ "audio": 0, - │ "reasoning": 0 - │ }, - │ "output_tokens": 21, - │ "total_tokens": 97 - │ } - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "text": "" - │ } - │ ] - │ ], - │ "llmOutput": { - │ "tokenUsage": { - │ "completionTokens": 21, - │ "promptTokens": 76, - │ "totalTokens": 97 - │ } - │ } - │ } - │ metadata: { - │ "batch_size": 1, - │ "braintrust": { - │ "integration_name": "langchain-js", - │ "sdk_language": "javascript" - │ }, - │ "invocation_params": { - │ "max_tokens": 128, - │ "model": "gpt-4o-mini-2024-07-18", - │ "stream": false, - │ "temperature": 0, - │ "tools": [ - │ { - │ "function": { - │ "description": "Perform a mathematical calculation", - │ "name": "calculate", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "a": { - │ "type": "number" - │ }, - │ "b": { - │ "type": "number" - │ }, - │ "operation": { - │ "enum": [ - │ "add", - │ "subtract", - │ "multiply", - │ "divide" - │ ], - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "operation", - │ "a", - │ "b" - │ ], - │ "type": "object" - │ } - │ }, - │ "type": "function" - │ } - │ ] - │ }, - │ "metadata": { - │ "ls_integration": "langchain_chat_model", - │ "ls_max_tokens": 128, - │ "ls_model_name": "gpt-4o-mini-2024-07-18", - │ "ls_model_type": "chat", - │ "ls_provider": "openai", - │ "ls_temperature": 0, - │ "versions": { - │ "@langchain/core": "1.2.1", - │ "@langchain/openai": "1.3.0" - │ } - │ }, - │ "model": "gpt-4o-mini-2024-07-18", - │ "options": { - │ "tools": [ - │ { - │ "function": { - │ "description": "Perform a mathematical calculation", - │ "name": "calculate", - │ "parameters": { - │ "$schema": "http://json-schema.org/draft-07/schema#", - │ "additionalProperties": false, - │ "properties": { - │ "a": { - │ "type": "number" - │ }, - │ "b": { - │ "type": "number" - │ }, - │ "operation": { - │ "enum": [ - │ "add", - │ "subtract", - │ "multiply", - │ "divide" - │ ], - │ "type": "string" - │ } - │ }, - │ "required": [ - │ "operation", - │ "a", - │ "b" - │ ], - │ "type": "object" - │ } - │ }, - │ "type": "function" - │ } - │ ] - │ }, - │ "run_id": "", - │ "serialized": { - │ "id": [ - │ "langchain", - │ "chat_models", - │ "openai", - │ "ChatOpenAI" - │ ], - │ "kwargs": { - │ "max_tokens": 128, - │ "model": "gpt-4o-mini-2024-07-18", - │ "openai_api_key": { - │ "id": [ - │ "OPENAI_API_KEY" - │ ], - │ "lc": 1, - │ "type": "secret" - │ }, - │ "temperature": 0 - │ }, - │ "lc": 1, - │ "type": "constructor" - │ }, - │ "tags": [] - │ } - │ metrics: { - │ "completion_reasoning_tokens": 0, - │ "completion_tokens": 21, - │ "prompt_cached_tokens": 0, - │ "prompt_tokens": 76, - │ "tokens": 97 - │ } - └── ChatOpenAI [llm] - input: [ - [ - { - "id": [ - "langchain_core", - "messages", - "HumanMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "What is 127 multiplied by 49? Use the calculate tool.", - "response_metadata": {} - }, - "lc": 1, - "type": "constructor" - }, - { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "", - "invalid_tool_calls": [], - "response_metadata": {}, - "tool_calls": [ - { - "args": { - "a": 127, - "b": 49, - "operation": "multiply" - }, - "id": "", - "name": "calculate", - "type": "tool_call" - } - ] - }, - "lc": 1, - "type": "constructor" - }, - { - "id": [ - "langchain_core", - "messages", - "ToolMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "6223", - "response_metadata": {}, - "tool_call_id": "call_dXQI5d9UAYuRygDqNMunj2W2" - }, - "lc": 1, - "type": "constructor" - } - ] - ] - output: { - "generations": [ - [ - { - "generationInfo": { - "finish_reason": "stop" - }, - "message": { - "id": [ - "langchain_core", - "messages", - "AIMessage" - ], - "kwargs": { - "additional_kwargs": {}, - "content": "127 multiplied by 49 is 6223.", - "id": "", - "invalid_tool_calls": [], - "response_metadata": { - "finish_reason": "stop", - "model_name": "gpt-4o-mini-2024-07-18", - "model_provider": "openai", - "system_fingerprint": "", - "tokenUsage": { - "completionTokens": 11, - "promptTokens": 106, - "totalTokens": 117 - }, - "usage": { - "completion_tokens": 11, - "completion_tokens_details": { - "accepted_prediction_tokens": 0, - "audio_tokens": 0, - "reasoning_tokens": 0, - "rejected_prediction_tokens": 0 - }, - "prompt_tokens": 106, - "prompt_tokens_details": { - "audio_tokens": 0, - "cached_tokens": 0 - }, - "total_tokens": 117 - } - }, - "tool_calls": [], - "type": "ai", - "usage_metadata": { - "input_token_details": { - "audio": 0, - "cache_read": 0 - }, - "input_tokens": 106, - "output_token_details": { - "audio": 0, - "reasoning": 0 - }, - "output_tokens": 11, - "total_tokens": 117 - } - }, - "lc": 1, - "type": "constructor" - }, - "text": "127 multiplied by 49 is 6223." - } - ] - ], - "llmOutput": { - "tokenUsage": { - "completionTokens": 11, - "promptTokens": 106, - "totalTokens": 117 - } - } - } - metadata: { - "batch_size": 1, - "braintrust": { - "integration_name": "langchain-js", - "sdk_language": "javascript" - }, - "invocation_params": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "stream": false, - "temperature": 0, - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "metadata": { - "ls_integration": "langchain_chat_model", - "ls_max_tokens": 128, - "ls_model_name": "gpt-4o-mini-2024-07-18", - "ls_model_type": "chat", - "ls_provider": "openai", - "ls_temperature": 0, - "versions": { - "@langchain/core": "1.2.1", - "@langchain/openai": "1.3.0" - } - }, - "model": "gpt-4o-mini-2024-07-18", - "options": { - "tools": [ - { - "function": { - "description": "Perform a mathematical calculation", - "name": "calculate", - "parameters": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "a": { - "type": "number" - }, - "b": { - "type": "number" - }, - "operation": { - "enum": [ - "add", - "subtract", - "multiply", - "divide" - ], - "type": "string" - } - }, - "required": [ - "operation", - "a", - "b" - ], - "type": "object" - } - }, - "type": "function" - } - ] - }, - "run_id": "", - "serialized": { - "id": [ - "langchain", - "chat_models", - "openai", - "ChatOpenAI" - ], - "kwargs": { - "max_tokens": 128, - "model": "gpt-4o-mini-2024-07-18", - "openai_api_key": { - "id": [ - "OPENAI_API_KEY" - ], - "lc": 1, - "type": "secret" - }, - "temperature": 0 - }, - "lc": 1, - "type": "constructor" - }, - "tags": [] - } - metrics: { - "completion_reasoning_tokens": 0, - "completion_tokens": 11, - "prompt_cached_tokens": 0, - "prompt_tokens": 106, - "tokens": 117 - } diff --git a/e2e/scenarios/wrap-langchain-js-traces/assertions.ts b/e2e/scenarios/wrap-langchain-js-traces/assertions.ts deleted file mode 100644 index aed4ce95b..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/assertions.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { expect } from "vitest"; -import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; -import { findChildSpans, findLatestSpan } from "../../helpers/trace-selectors"; - -function findNamedChildSpan( - capturedEvents: CapturedLogEvent[], - names: string[], - parentId: string | undefined, -) { - for (const name of names) { - const span = findChildSpans(capturedEvents, name, parentId)[0]; - if (span) { - return span; - } - } - - return undefined; -} - -export function assertLangchainTraces(options: { - capturedEvents: CapturedLogEvent[]; - rootName: string; - scenarioName: string; -}): CapturedLogEvent[] { - const root = findLatestSpan(options.capturedEvents, options.rootName); - const invokeOperation = findLatestSpan( - options.capturedEvents, - "langchain-invoke-operation", - ); - const chainOperation = findLatestSpan( - options.capturedEvents, - "langchain-chain-operation", - ); - const streamOperation = findLatestSpan( - options.capturedEvents, - "langchain-stream-operation", - ); - const toolOperation = findLatestSpan( - options.capturedEvents, - "langchain-tool-operation", - ); - const toolResultOperation = findLatestSpan( - options.capturedEvents, - "langchain-tool-result-operation", - ); - - expect(root).toBeDefined(); - expect(invokeOperation).toBeDefined(); - expect(chainOperation).toBeDefined(); - expect(streamOperation).toBeDefined(); - expect(toolOperation).toBeDefined(); - expect(toolResultOperation).toBeDefined(); - - expect(root?.row.metadata).toMatchObject({ - scenario: options.scenarioName, - }); - - for (const operation of [ - invokeOperation, - chainOperation, - streamOperation, - toolOperation, - toolResultOperation, - ]) { - expect(operation?.span.parentIds).toEqual([root?.span.id ?? ""]); - } - - const invokeSpan = findNamedChildSpan( - options.capturedEvents, - ["ChatOpenAI"], - invokeOperation?.span.id, - ); - expect(invokeSpan).toBeDefined(); - expect(invokeSpan?.span.type).toBe("llm"); - expect(invokeSpan?.metrics?.completion_reasoning_tokens).toBeGreaterThan(0); - - const chainChildren = findChildSpans( - options.capturedEvents, - "RunnableSequence", - chainOperation?.span.id, - ); - expect(chainChildren.length).toBeGreaterThanOrEqual(1); - - const streamSpan = findNamedChildSpan( - options.capturedEvents, - ["ChatOpenAI"], - streamOperation?.span.id, - ); - expect(streamSpan).toBeDefined(); - expect(streamSpan?.span.type).toBe("llm"); - expect(streamSpan?.metrics).toMatchObject({ - time_to_first_token: expect.any(Number), - }); - - const toolSpan = findNamedChildSpan( - options.capturedEvents, - ["ChatOpenAI"], - toolOperation?.span.id, - ); - expect(toolSpan).toBeDefined(); - - const toolOutputStr = JSON.stringify(toolSpan?.output ?? {}); - expect(toolOutputStr).toContain("get_weather"); - - const toolResultSpans = findChildSpans( - options.capturedEvents, - "ChatOpenAI", - toolResultOperation?.span.id, - ); - expect(toolResultSpans.length).toBeGreaterThanOrEqual(2); - - return options.capturedEvents; -} diff --git a/e2e/scenarios/wrap-langchain-js-traces/package.json b/e2e/scenarios/wrap-langchain-js-traces/package.json deleted file mode 100644 index 7ef2d95ee..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@braintrust/e2e-wrap-langchain-js-traces", - "private": true, - "braintrustScenario": { - "bump": { - "dependencies": { - "langchain-core-v1-latest": { - "package": "@langchain/core", - "range": "1" - }, - "langchain-openai-v1-latest": { - "package": "@langchain/openai", - "range": "1" - } - } - } - }, - "dependencies": { - "zod": "3.25.76", - "langchain-core-v1": "npm:@langchain/core@1.1.35", - "langchain-core-v1-latest": "npm:@langchain/core@1.2.1", - "langchain-openai-v1": "npm:@langchain/openai@1.3.0", - "langchain-openai-v1-latest": "npm:@langchain/openai@1.5.3" - }, - "pnpm": { - "overrides": { - "langsmith@<0.3.30": "^0.3.30", - "uuid": "^11.1.1" - } - } -} diff --git a/e2e/scenarios/wrap-langchain-js-traces/pnpm-lock.yaml b/e2e/scenarios/wrap-langchain-js-traces/pnpm-lock.yaml deleted file mode 100644 index 5dcbf1544..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/pnpm-lock.yaml +++ /dev/null @@ -1,296 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - langsmith@<0.3.30: ^0.3.30 - uuid: ^11.1.1 - -importers: - - .: - dependencies: - langchain-core-v1-latest: - specifier: npm:@langchain/core@1.2.1 - version: '@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76))' - langchain-core-v1: - specifier: npm:@langchain/core@1.1.35 - version: '@langchain/core@1.1.35(openai@6.45.0(zod@3.25.76))' - langchain-openai-v1-latest: - specifier: npm:@langchain/openai@1.5.3 - version: '@langchain/openai@1.5.3(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76)))' - langchain-openai-v1: - specifier: npm:@langchain/openai@1.3.0 - version: '@langchain/openai@1.3.0(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76)))' - zod: - specifier: 3.25.76 - version: 3.25.76 - -packages: - - '@cfworker/json-schema@4.1.1': - resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} - - '@langchain/core@1.1.35': - resolution: {integrity: sha512-TM0idLSAx17IEMqEIN7F8MDdXpN53p4A4vhhjZxf7LedwShB/8Zx1F5D5Nq1Bbn0zh9QDzSHpWyfCrTtggKf7g==} - engines: {node: '>=20'} - - '@langchain/core@1.2.1': - resolution: {integrity: sha512-NNG/cC5FGuHDOAP56h0ddp8Rfk8p+othWzEK5RV9JIG6RvnF5vGa5r0AEGtKfQieed7s1kC42GuIzVOBvMBL/g==} - engines: {node: '>=20'} - - '@langchain/openai@1.3.0': - resolution: {integrity: sha512-FDsF6xKCvFduiZcX57fL2Md+DZ+fJubcUN1iwUaEwJOQnq7zFFYj3a/KuQ7EiOFR3hEsnhPilSfxO1VW85wMZw==} - engines: {node: '>=20'} - peerDependencies: - '@langchain/core': ^1.1.33 - - '@langchain/openai@1.5.3': - resolution: {integrity: sha512-OStS2AUvy9oe/hEf/3ndBOFztUDOfuJYLNXh89m3iiJAI2Cp5Dp0n/pvpO27MO0b+VgENd+xSHVyQZ7fe+ulxg==} - engines: {node: '>=20'} - peerDependencies: - '@langchain/core': ^1.2.1 - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@types/uuid@10.0.0': - resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - console-table-printer@2.15.0: - resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==} - - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - - js-tiktoken@1.0.21: - resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} - - langsmith@0.5.11: - resolution: {integrity: sha512-Yio502Ow2vbVt16P1sybNMNpMsr5BMqoeonoi4flrcDsP55No/aCe2zydtBNOv0+kjKQw4WSKAzTsNwenDeD5w==} - 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 - - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - - openai@6.32.0: - resolution: {integrity: sha512-j3k+BjydAf8yQlcOI7WUQMQTbbF5GEIMAE2iZYCOzwwB3S2pCheaWYp+XZRNAch4jWVc52PMDGRRjutao3lLCg==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - - openai@6.45.0: - resolution: {integrity: sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==} - peerDependencies: - '@aws-sdk/credential-provider-node': '>=3.972.0 <4' - '@smithy/hash-node': '>=4.3.0 <5' - '@smithy/signature-v4': '>=5.4.0 <6' - ws: ^8.18.0 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@aws-sdk/credential-provider-node': - optional: true - '@smithy/hash-node': - optional: true - '@smithy/signature-v4': - optional: true - ws: - optional: true - zod: - 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-timeout@3.2.0: - resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} - engines: {node: '>=8'} - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - simple-wcswidth@1.1.2: - resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} - - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} - hasBin: true - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - -snapshots: - - '@cfworker/json-schema@4.1.1': {} - - '@langchain/core@1.1.35(openai@6.45.0(zod@3.25.76))': - dependencies: - '@cfworker/json-schema': 4.1.1 - '@standard-schema/spec': 1.1.0 - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.21 - langsmith: 0.5.11(openai@6.45.0(zod@3.25.76)) - mustache: 4.2.0 - p-queue: 6.6.2 - uuid: 11.1.1 - zod: 3.25.76 - transitivePeerDependencies: - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - openai - - ws - - '@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76))': - dependencies: - '@cfworker/json-schema': 4.1.1 - '@standard-schema/spec': 1.1.0 - js-tiktoken: 1.0.21 - langsmith: 0.5.11(openai@6.45.0(zod@3.25.76)) - mustache: 4.2.0 - p-queue: 6.6.2 - zod: 3.25.76 - transitivePeerDependencies: - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - openai - - ws - - '@langchain/openai@1.3.0(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76)))': - dependencies: - '@langchain/core': 1.2.1(openai@6.45.0(zod@3.25.76)) - js-tiktoken: 1.0.21 - openai: 6.32.0(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - ws - - '@langchain/openai@1.5.3(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76)))': - dependencies: - '@langchain/core': 1.2.1(openai@6.45.0(zod@3.25.76)) - js-tiktoken: 1.0.21 - openai: 6.45.0(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - '@aws-sdk/credential-provider-node' - - '@smithy/hash-node' - - '@smithy/signature-v4' - - ws - - '@standard-schema/spec@1.1.0': {} - - '@types/uuid@10.0.0': {} - - ansi-styles@5.2.0: {} - - base64-js@1.5.1: {} - - camelcase@6.3.0: {} - - chalk@5.6.2: {} - - console-table-printer@2.15.0: - dependencies: - simple-wcswidth: 1.1.2 - - decamelize@1.2.0: {} - - eventemitter3@4.0.7: {} - - js-tiktoken@1.0.21: - dependencies: - base64-js: 1.5.1 - - langsmith@0.5.11(openai@6.45.0(zod@3.25.76)): - dependencies: - '@types/uuid': 10.0.0 - chalk: 5.6.2 - console-table-printer: 2.15.0 - p-queue: 6.6.2 - semver: 7.7.4 - uuid: 11.1.1 - optionalDependencies: - openai: 6.45.0(zod@3.25.76) - - mustache@4.2.0: {} - - openai@6.32.0(zod@3.25.76): - optionalDependencies: - zod: 3.25.76 - - openai@6.45.0(zod@3.25.76): - optionalDependencies: - zod: 3.25.76 - - p-finally@1.0.0: {} - - p-queue@6.6.2: - dependencies: - eventemitter3: 4.0.7 - p-timeout: 3.2.0 - - p-timeout@3.2.0: - dependencies: - p-finally: 1.0.0 - - semver@7.7.4: {} - - simple-wcswidth@1.1.2: {} - - uuid@11.1.1: {} - - zod@3.25.76: {} diff --git a/e2e/scenarios/wrap-langchain-js-traces/scenario.test.ts b/e2e/scenarios/wrap-langchain-js-traces/scenario.test.ts deleted file mode 100644 index db2ed6d51..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/scenario.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { expect, test } from "vitest"; -import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; -import { - prepareScenarioDir, - readInstalledPackageVersion, - resolveScenarioDir, - withScenarioHarness, -} from "../../helpers/scenario-harness"; -import { matchSpanTreeSnapshot } from "../../helpers/span-tree"; - -import { assertLangchainTraces } from "./assertions"; - -const originalScenarioDir = resolveScenarioDir(import.meta.url); -const scenarioDir = await prepareScenarioDir({ - scenarioDir: originalScenarioDir, -}); -const TIMEOUT_MS = 90_000; -const scenarios = await Promise.all( - [ - { - coreDependencyName: "langchain-core-v1", - openAIDependencyName: "langchain-openai-v1", - variantKey: "wrap-langchain-js-traces-v1", - }, - { - coreDependencyName: "langchain-core-v1-latest", - openAIDependencyName: "langchain-openai-v1-latest", - variantKey: "wrap-langchain-js-traces-v1-latest", - }, - ].map(async (scenario) => ({ - ...scenario, - version: await readInstalledPackageVersion( - scenarioDir, - scenario.openAIDependencyName, - ), - })), -); - -for (const scenario of scenarios) { - test( - `wrap-langchain-js-traces captures invoke, chain, stream, and tool spans via BraintrustCallbackHandler (${scenario.version})`, - { - timeout: TIMEOUT_MS, - }, - async () => { - await withScenarioHarness(async ({ events, runScenarioDir }) => { - await runScenarioDir({ - scenarioDir, - timeoutMs: TIMEOUT_MS, - env: { - LANGCHAIN_CORE_PACKAGE_NAME: scenario.coreDependencyName, - LANGCHAIN_OPENAI_PACKAGE_NAME: scenario.openAIDependencyName, - }, - runContext: { - variantKey: scenario.variantKey, - originalScenarioDir, - }, - }); - - const spanTree = assertLangchainTraces({ - capturedEvents: events(), - rootName: "langchain-wrapper-root", - scenarioName: "wrap-langchain-js-traces", - }); - - await matchSpanTreeSnapshot( - spanTree, - resolveFileSnapshotPath( - import.meta.url, - `${scenario.variantKey}.span-tree.json`, - ), - ); - }); - }, - ); -} diff --git a/e2e/scenarios/wrap-langchain-js-traces/scenario.ts b/e2e/scenarios/wrap-langchain-js-traces/scenario.ts deleted file mode 100644 index ffa07bf2e..000000000 --- a/e2e/scenarios/wrap-langchain-js-traces/scenario.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { BraintrustCallbackHandler } from "@braintrust/langchain-js"; -const langchainCorePackageName = - process.env.LANGCHAIN_CORE_PACKAGE_NAME ?? "langchain-core-v1-latest"; -const langchainOpenAIPackageName = - process.env.LANGCHAIN_OPENAI_PACKAGE_NAME ?? "langchain-openai-v1-latest"; -import { z } from "zod"; -import { - collectAsync, - runOperation, - runTracedScenario, -} from "../../helpers/provider-runtime.mjs"; -import { runMain } from "../../helpers/scenario-runtime"; - -const OPENAI_MODEL = "gpt-4o-mini-2024-07-18"; -const OPENAI_REASONING_MODEL = "gpt-5-nano"; - -runMain(async () => { - const { AIMessage, HumanMessage, ToolMessage } = await import( - `${langchainCorePackageName}/messages` - ); - const { ChatPromptTemplate } = await import( - `${langchainCorePackageName}/prompts` - ); - const { DynamicStructuredTool } = await import( - `${langchainCorePackageName}/tools` - ); - const { ChatOpenAI } = await import(langchainOpenAIPackageName); - - await runTracedScenario({ - callback: async () => { - // The handler attaches as a child of the current traced span via - // the parent option resolved by runTracedScenario. - const handler = new BraintrustCallbackHandler(); - - await runOperation("langchain-invoke-operation", "invoke", async () => { - const model = new ChatOpenAI({ - model: OPENAI_REASONING_MODEL, - maxTokens: 512, - callbacks: [handler], - }); - await model.invoke([new HumanMessage("Reply with exactly OK.")]); - }); - - await runOperation("langchain-chain-operation", "chain", async () => { - const model = new ChatOpenAI({ - model: OPENAI_MODEL, - maxTokens: 32, - temperature: 0, - }); - const prompt = ChatPromptTemplate.fromTemplate( - "Reply with the single word {word} and nothing else.", - ); - const chain = prompt.pipe(model); - await chain.invoke({ word: "PARIS" }, { callbacks: [handler] }); - }); - - await runOperation("langchain-stream-operation", "stream", async () => { - const model = new ChatOpenAI({ - model: OPENAI_MODEL, - maxTokens: 32, - temperature: 0, - streaming: true, - callbacks: [handler], - }); - const stream = await model.stream([ - new HumanMessage( - "Count from 1 to 3 and include the words one two three.", - ), - ]); - await collectAsync(stream); - }); - - await runOperation("langchain-tool-operation", "tool", async () => { - const getWeatherTool = new DynamicStructuredTool({ - name: "get_weather", - description: "Get the current weather in a given location", - schema: z.object({ - location: z - .string() - .describe("The city and state or city and country"), - }), - func: async ({ location }) => { - return JSON.stringify({ - condition: "sunny", - location, - temperatureC: 22, - }); - }, - }); - - const model = new ChatOpenAI({ - model: OPENAI_MODEL, - maxTokens: 128, - temperature: 0, - callbacks: [handler], - }); - const modelWithTools = model.bindTools([getWeatherTool]); - await modelWithTools.invoke( - "Use the get_weather tool for Paris, France. Do not answer from memory.", - ); - }); - - await runOperation( - "langchain-tool-result-operation", - "tool-result", - async () => { - const calculateTool = new DynamicStructuredTool({ - name: "calculate", - description: "Perform a mathematical calculation", - schema: z.object({ - operation: z.enum(["add", "subtract", "multiply", "divide"]), - a: z.number(), - b: z.number(), - }), - func: async ({ operation, a, b }) => { - const ops = { - add: a + b, - subtract: a - b, - multiply: a * b, - divide: b !== 0 ? a / b : 0, - }; - return String(ops[operation]); - }, - }); - - const model = new ChatOpenAI({ - model: OPENAI_MODEL, - maxTokens: 128, - temperature: 0, - callbacks: [handler], - }); - const modelWithTools = model.bindTools([calculateTool]); - - const firstResult = await modelWithTools.invoke( - "What is 127 multiplied by 49? Use the calculate tool.", - ); - - if (firstResult.tool_calls && firstResult.tool_calls.length > 0) { - const toolCall = firstResult.tool_calls[0]; - const result = String(127 * 49); - - await modelWithTools.invoke([ - new HumanMessage( - "What is 127 multiplied by 49? Use the calculate tool.", - ), - new AIMessage({ content: "", tool_calls: [toolCall] }), - new ToolMessage({ - content: result, - tool_call_id: toolCall.id, - }), - ]); - } - }, - ); - }, - metadata: { - scenario: "wrap-langchain-js-traces", - }, - projectNameBase: "e2e-wrap-langchain", - rootName: "langchain-wrapper-root", - }); -}); diff --git a/integrations/langchain-js/CHANGELOG.md b/integrations/langchain-js/CHANGELOG.md deleted file mode 100644 index 2ab7af78b..000000000 --- a/integrations/langchain-js/CHANGELOG.md +++ /dev/null @@ -1,31 +0,0 @@ -# @braintrust/langchain-js - -## 0.2.5 - -- `@braintrust/langchain-js` is now deprecated. This release marks the last release for this package. (https://github.com/braintrustdata/braintrust-sdk-javascript/pull/2383) -- Updated dependencies: braintrust@3.29.0 - -### Migration - -To migrate, install `braintrust` and import `BraintrustLangChainCallbackHandler` -from `braintrust` instead. See the -[LangChain integration guide](https://www.braintrust.dev/docs/integrations/sdk-integrations/langchain) -for setup examples. For application-wide instrumentation, use the Braintrust -runtime hook described in the -[auto-instrumentation guide](https://www.braintrust.dev/docs/instrument/trace-llm-calls#auto-instrumentation). -`setGlobalHandler` has no direct replacement; use automatic instrumentation or -pass the callback handler explicitly. - -## 0.2.4 - -### Patch Changes - -- feat: Add LangChain and LangGraph auto-instrumentation (https://github.com/braintrustdata/braintrust-sdk-javascript/pull/1897) -- Updated dependencies: braintrust@3.12.0 - -## 0.2.3 - -### Patch Changes - -- Added prompt caching token tracking for LangChain usage metadata. -- Mapped nested `input_token_details` metrics into Braintrust's standard cached-token fields, including cache reads and cache creation tokens. diff --git a/integrations/langchain-js/README.md b/integrations/langchain-js/README.md deleted file mode 100644 index 93f716031..000000000 --- a/integrations/langchain-js/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# @braintrust/langchain-js - -[![npm version](https://img.shields.io/npm/v/%40braintrust%2Flangchain-js.svg)](https://www.npmjs.com/package/@braintrust/langchain-js) - -> [!WARNING] -> This package and all of its exports are deprecated and will stop being -> published after the next release. Use the LangChain integration in -> `braintrust` instead. - -## Installation - -```bash -npm install braintrust -# or -yarn add braintrust -# or -pnpm add braintrust -``` - -## Automatic instrumentation - -For application-wide instrumentation, use the Braintrust runtime hook: - -```bash -node --import braintrust/hook.mjs app.js -``` - -Braintrust also supports Vite, Webpack, esbuild, and Rollup. See the -[Braintrust SDK documentation](../../js/README.md#auto-instrumentation) for -bundler setup. - -## Manual instrumentation - -For scoped instrumentation, pass `BraintrustLangChainCallbackHandler` from -`braintrust` through LangChain's `callbacks` option: - -```typescript -import { ChatOpenAI } from "@langchain/openai"; -import { BraintrustLangChainCallbackHandler } from "braintrust"; - -const handler = new BraintrustLangChainCallbackHandler(); - -const model = new ChatOpenAI({ - callbacks: [handler], -}); - -const response = await model.invoke("Tell me a joke about bears", { - callbacks: [handler], -}); -``` - -`setGlobalHandler` does not have a direct replacement in `braintrust`. Use -automatic instrumentation for application-wide coverage, or pass the handler -explicitly for scoped instrumentation. - -## Documentation - -- Braintrust docs: [https://www.braintrust.dev/docs](https://www.braintrust.dev/docs) -- LangChain callback docs: [https://js.langchain.com/docs/how_to/#callbacks](https://js.langchain.com/docs/how_to/#callbacks) diff --git a/integrations/langchain-js/package.json b/integrations/langchain-js/package.json deleted file mode 100644 index 36e1b88f1..000000000 --- a/integrations/langchain-js/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "@braintrust/langchain-js", - "version": "0.2.5", - "description": "SDK for integrating Braintrust with LangChain.js", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "scripts": { - "build": "tsup", - "check:typings": "tsc --noEmit", - "watch": "tsup --watch", - "clean": "rm -r dist/*", - "test": "vitest run", - "yalc:publish": "yalc publish" - }, - "author": "Braintrust Data Inc.", - "license": "MIT", - "devDependencies": { - "@langchain/anthropic": "^1.3.1", - "@langchain/core": "^1.1.6", - "@langchain/langgraph": "^1.0.7", - "@langchain/openai": "^1.2.0", - "@types/node": "^20.10.5", - "braintrust": "workspace:*", - "msw": "^2.6.6", - "tsup": "^8.5.1", - "typescript": "^5.3.3", - "vitest": "4.1.5", - "zod": "^3.25.34", - "zod-to-json-schema": "^3.22.5" - }, - "peerDependencies": { - "@langchain/core": ">=0.3.42", - "braintrust": ">=0.4.0" - }, - "peerDependenciesMeta": { - "braintrust": { - "optional": false - }, - "@langchain/core": { - "optional": false - } - }, - "repository": { - "type": "git", - "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", - "directory": "integrations/langchain-js" - }, - "homepage": "https://www.braintrust.dev/docs", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/", - "provenance": true - } -} diff --git a/integrations/langchain-js/src/BraintrustCallbackHandler.fixtures.ts b/integrations/langchain-js/src/BraintrustCallbackHandler.fixtures.ts deleted file mode 100644 index f7ec70434..000000000 --- a/integrations/langchain-js/src/BraintrustCallbackHandler.fixtures.ts +++ /dev/null @@ -1,234 +0,0 @@ -export const CHAT_MATH = { - id: "chatcmpl-Aao716hWOt9HBihjWh9iAPGWRpkFd", - object: "chat.completion", - created: 1733335803, - model: "gpt-4o-mini-2024-07-18", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: "1 + 2 equals 3.", - refusal: null, - }, - logprobs: null, - finish_reason: "stop", - }, - ], - usage: { - prompt_tokens: 15, - completion_tokens: 8, - total_tokens: 23, - prompt_tokens_details: { - cached_tokens: 0, - audio_tokens: 0, - }, - completion_tokens_details: { - reasoning_tokens: 0, - audio_tokens: 0, - accepted_prediction_tokens: 0, - rejected_prediction_tokens: 0, - }, - }, - system_fingerprint: "fp_0705bf87c0", -}; - -export const CHAT_STREAM_PARROT = [ - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}`, - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":"Pol"},"logprobs":null,"finish_reason":null}],"usage":null}`, - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":"ly"},"logprobs":null,"finish_reason":null}],"usage":null}`, - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":" wants"},"logprobs":null,"finish_reason":null}],"usage":null}`, - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null}`, - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":" crackers"},"logprobs":null,"finish_reason":null}],"usage":null}`, - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":"!"},"logprobs":null,"finish_reason":null}],"usage":null}`, - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null}`, - `data: {"id":"chatcmpl-Ab9p7esnOlnH4ywBeHQOp4ScoCcde","object":"chat.completion.chunk","created":1733419261,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0705bf87c0","choices":[],"usage":{"prompt_tokens":16,"completion_tokens":6,"total_tokens":22,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}`, - `data: [DONE]`, -]; - -export const CHAT_CHAIN_MEMORY = { - id: "chatcmpl-AbAHIqtiUXMz849pZPWxB7RKF9wPh", - object: "chat.completion", - created: 1733421008, - model: "gpt-4o-mini-2024-07-18", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: "Assistant: I'm called Assistant! How can I help you today?", - refusal: null, - }, - logprobs: null, - finish_reason: "stop", - }, - ], - usage: { - prompt_tokens: 24, - completion_tokens: 13, - total_tokens: 37, - prompt_tokens_details: { - cached_tokens: 0, - audio_tokens: 0, - }, - completion_tokens_details: { - reasoning_tokens: 0, - audio_tokens: 0, - accepted_prediction_tokens: 0, - rejected_prediction_tokens: 0, - }, - }, - system_fingerprint: "fp_0705bf87c0", -}; - -export const CHAT_TOOL_CALCULATOR = { - id: "chatcmpl-AbAVR1TojvbDgXRLlDyhz9NYZVitz", - object: "chat.completion", - created: 1733421885, - model: "gpt-4o-mini-2024-07-18", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: null, - tool_calls: [ - { - id: "call_G2Qd8HzTMyFUiMafz5H4fBIi", - type: "function", - function: { - name: "calculator", - arguments: '{"operation":"multiply","number1":3,"number2":12}', - }, - }, - ], - refusal: null, - }, - logprobs: null, - finish_reason: "tool_calls", - }, - ], - usage: { - prompt_tokens: 93, - completion_tokens: 24, - total_tokens: 117, - prompt_tokens_details: { - cached_tokens: 0, - audio_tokens: 0, - }, - completion_tokens_details: { - reasoning_tokens: 0, - audio_tokens: 0, - accepted_prediction_tokens: 0, - rejected_prediction_tokens: 0, - }, - }, - system_fingerprint: "fp_0705bf87c0", -}; - -export const CHAT_BEAR_JOKE = { - id: "chatcmpl-AbCj2kznx4QsGpaocNir4GWdLYYqj", - object: "chat.completion", - created: 1733430416, - model: "gpt-4o-mini-2024-07-18", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: - 'Why did the bear sit on the log?\n\nBecause it wanted to be a "bear-ly" seated customer! 🐻', - refusal: null, - }, - logprobs: null, - finish_reason: "stop", - }, - ], - usage: { - prompt_tokens: 13, - completion_tokens: 26, - total_tokens: 39, - prompt_tokens_details: { - cached_tokens: 0, - audio_tokens: 0, - }, - completion_tokens_details: { - reasoning_tokens: 0, - audio_tokens: 0, - accepted_prediction_tokens: 0, - rejected_prediction_tokens: 0, - }, - }, - system_fingerprint: "fp_bba3c8e70b", -}; - -export const CHAT_BEAR_POEM = { - id: "chatcmpl-AbClwtnbeqLRiWwoe21On10TRqqsW", - object: "chat.completion", - created: 1733430596, - model: "gpt-4o-mini-2024-07-18", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: - "In the forest's hush, a shadow moves near, \nA gentle giant roams, the wise old bear.", - refusal: null, - }, - logprobs: null, - finish_reason: "stop", - }, - ], - usage: { - prompt_tokens: 15, - completion_tokens: 23, - total_tokens: 38, - prompt_tokens_details: { - cached_tokens: 0, - audio_tokens: 0, - }, - completion_tokens_details: { - reasoning_tokens: 0, - audio_tokens: 0, - accepted_prediction_tokens: 0, - rejected_prediction_tokens: 0, - }, - }, - system_fingerprint: "fp_bba3c8e70b", -}; - -export const CHAT_SAY_HELLO = { - id: "chatcmpl-AbFX2khtngBETl7qtntuXawF6RFPt", - object: "chat.completion", - created: 1733441204, - model: "gpt-4o-mini-2024-07-18", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: "Hello! How can I assist you today?", - refusal: null, - }, - logprobs: null, - finish_reason: "stop", - }, - ], - usage: { - prompt_tokens: 9, - completion_tokens: 9, - total_tokens: 18, - prompt_tokens_details: { - cached_tokens: 0, - audio_tokens: 0, - }, - completion_tokens_details: { - reasoning_tokens: 0, - audio_tokens: 0, - accepted_prediction_tokens: 0, - rejected_prediction_tokens: 0, - }, - }, - system_fingerprint: "fp_818c284075", -}; diff --git a/integrations/langchain-js/src/BraintrustCallbackHandler.test.ts b/integrations/langchain-js/src/BraintrustCallbackHandler.test.ts deleted file mode 100644 index e45afe2e6..000000000 --- a/integrations/langchain-js/src/BraintrustCallbackHandler.test.ts +++ /dev/null @@ -1,1239 +0,0 @@ -import { ChatAnthropic } from "@langchain/anthropic"; -import { HumanMessage, SystemMessage } from "@langchain/core/messages"; -import { ChatPromptTemplate, PromptTemplate } from "@langchain/core/prompts"; -import { RunnableMap } from "@langchain/core/runnables"; -import { tool } from "@langchain/core/tools"; -import { Annotation, END, START, StateGraph } from "@langchain/langgraph"; -import { ChatOpenAI } from "@langchain/openai"; -import { flush, initLogger, NOOP_SPAN } from "braintrust"; -import { http, HttpResponse } from "msw"; -import { ReadableStream } from "stream/web"; -import { describe, expect, it } from "vitest"; -import { z } from "zod/v3"; - -import { BraintrustCallbackHandler } from "./BraintrustCallbackHandler"; -import { - CHAT_BEAR_JOKE, - CHAT_BEAR_POEM, - CHAT_CHAIN_MEMORY, - CHAT_MATH, - CHAT_SAY_HELLO, - CHAT_STREAM_PARROT, - CHAT_TOOL_CALCULATOR, -} from "./BraintrustCallbackHandler.fixtures"; -import { server } from "./test/setup"; -import { LogsRequest } from "./test/types"; -import { logsToSpans, withLogging } from "./test/utils"; - -initLogger({ - projectName: "langchain", -}); - -const handler = withLogging(new BraintrustCallbackHandler({ debug: true })); - -const encoder = new TextEncoder(); - -describe("BraintrustCallbackHandler", () => { - it("should handle LLM calls", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post("https://api.openai.com/v1/chat/completions", () => { - return HttpResponse.json(CHAT_MATH); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["4bc6305f-2175-4481-bc84-7c55a456b7ea"]); - }), - ); - - const prompt = ChatPromptTemplate.fromTemplate(`What is 1 + {number}?`); - const model = new ChatOpenAI({ - model: "gpt-4o-mini-2024-07-18", - }); - - const chain = prompt.pipe(model); - - const message = await chain.invoke( - { number: "2" }, - { callbacks: [handler] }, - ); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - expect(spans).toMatchObject([ - { - span_id: root_span_id, - root_span_id: trace_id, - span_attributes: { - name: "RunnableSequence", - type: "task", - }, - input: { - number: "2", - }, - metadata: { - tags: [], - }, - output: { - kwargs: { - content: expect.any(String), // LLM response text - additional_kwargs: expect.any(Object), - response_metadata: expect.any(Object), - tool_calls: expect.any(Array), - invalid_tool_calls: expect.any(Array), - usage_metadata: expect.any(Object), - }, - }, - }, - { - span_attributes: { - name: "ChatPromptTemplate", - }, - input: { - number: "2", - }, - metadata: { - tags: ["seq:step:1"], - }, - output: { - kwargs: { - messages: [ - { - kwargs: { - content: expect.any(String), // Formatted prompt text - additional_kwargs: {}, - response_metadata: {}, - }, - }, - ], - }, - }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - { - span_attributes: { - name: "ChatOpenAI", - type: "llm", - }, - input: [ - [ - { - kwargs: { - content: expect.any(String), // Prompt message content - additional_kwargs: {}, - response_metadata: {}, - }, - }, - ], - ], - metrics: { - start: expect.any(Number), - tokens: expect.any(Number), - prompt_tokens: expect.any(Number), - completion_tokens: expect.any(Number), - end: expect.any(Number), - }, - metadata: { - tags: ["seq:step:2"], - model: "gpt-4o-mini-2024-07-18", - }, - output: { - generations: [ - [ - { - text: expect.any(String), // Generated text - message: { - kwargs: { - content: expect.any(String), // Message content - additional_kwargs: expect.any(Object), - response_metadata: expect.any(Object), - tool_calls: expect.any(Array), - invalid_tool_calls: expect.any(Array), - usage_metadata: expect.any(Object), - }, - }, - }, - ], - ], - llmOutput: { - tokenUsage: { - promptTokens: expect.any(Number), - completionTokens: expect.any(Number), - totalTokens: expect.any(Number), - }, - }, - }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - ]); - - expect(message.content).toEqual(expect.stringContaining("3")); - }); -}); - -it("should handle streaming LLM calls", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post("https://api.openai.com/v1/chat/completions", async () => { - const stream = new ReadableStream({ - start(controller) { - const chunks = CHAT_STREAM_PARROT; - - for (const chunk of chunks) { - controller.enqueue(encoder.encode(chunk + "\n\n")); - } - controller.close(); - }, - }); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream; charset=utf-8", - "Transfer-Encoding": "chunked", - }, - }); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["stream-span-id"]); - }), - ); - - const prompt = ChatPromptTemplate.fromTemplate( - `tell me a four word joke about {topic}`, - ); - const model = new ChatOpenAI({ - model: "gpt-4o-mini-2024-07-18", - streaming: true, - }); - - const chain = prompt.pipe(model); - - const stream = await chain.stream( - { topic: "parrot" }, - { callbacks: [handler] }, - ); - - const chunks = []; - for await (const chunk of stream) { - chunks.push(chunk); - } - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - expect(spans).toMatchObject([ - { - span_attributes: { - name: "RunnableSequence", - type: "task", - }, - input: { - topic: "parrot", - }, - metadata: { - tags: [], - }, - span_id: root_span_id, - root_span_id: trace_id, - }, - { - span_attributes: { name: "ChatPromptTemplate", type: "task" }, - input: { topic: "parrot" }, - output: expect.objectContaining({ - kwargs: expect.objectContaining({ - messages: expect.arrayContaining([ - expect.objectContaining({ - kwargs: expect.objectContaining({ - content: "tell me a four word joke about parrot", - }), - }), - ]), - }), - }), - metadata: { tags: ["seq:step:1"] }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - { - span_attributes: { name: "ChatOpenAI", type: "llm" }, - input: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - kwargs: expect.objectContaining({ - content: "tell me a four word joke about parrot", - }), - }), - ]), - ]), - output: expect.objectContaining({ - generations: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - message: expect.objectContaining({ - kwargs: expect.objectContaining({ - content: "Polly wants more crackers!", - }), - }), - }), - ]), - ]), - }), - metrics: { - completion_tokens: 6, - end: expect.any(Number), - prompt_tokens: 16, - start: expect.any(Number), - tokens: 22, - }, - metadata: { - tags: ["seq:step:2"], - model: "gpt-4o-mini-2024-07-18", - }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - ]); - - expect(chunks.length).toBeGreaterThan(0); -}); - -it("should track time-to-first-token in streaming calls", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post("https://api.openai.com/v1/chat/completions", async () => { - const stream = new ReadableStream({ - start(controller) { - const chunks = CHAT_STREAM_PARROT; - - for (const chunk of chunks) { - controller.enqueue(encoder.encode(chunk + "\n\n")); - } - controller.close(); - }, - }); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream; charset=utf-8", - "Transfer-Encoding": "chunked", - }, - }); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["stream-span-id"]); - }), - ); - - const prompt = ChatPromptTemplate.fromTemplate("Count from 1 to 5."); - const model = new ChatOpenAI({ - model: "gpt-4o-mini", - streaming: true, - }); - - const chain = prompt.pipe(model); - - const chunks = []; - const stream = await chain.stream({}, { callbacks: [handler] }); - for await (const chunk of stream) { - chunks.push(chunk); - } - - // Verify we got streaming chunks - expect(chunks.length).toBeGreaterThan(0); - - await flush(); - - const { spans } = logsToSpans(logs); - - // Find the LLM span - const llmSpan = spans.find( - (s) => - s.span_attributes?.name === "ChatOpenAI" && - s.span_attributes?.type === "llm", - ); - - expect(llmSpan).toBeDefined(); - expect(llmSpan?.metrics).toMatchObject({ - time_to_first_token: expect.any(Number), - prompt_tokens: expect.any(Number), - completion_tokens: expect.any(Number), - tokens: expect.any(Number), - }); - - // Verify TTFT is a reasonable value (positive and less than total time) - expect(llmSpan?.metrics?.time_to_first_token).toBeGreaterThan(0); -}); - -it("should handle multi-step chains with memory", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post("https://api.openai.com/v1/chat/completions", () => { - return HttpResponse.json(CHAT_CHAIN_MEMORY); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["memory-span-id"]); - }), - ); - - const prompt = ChatPromptTemplate.fromTemplate(`{history} User: {input}`); - const model = new ChatOpenAI({ - model: "gpt-4o-mini-2024-07-18", - }); - - const chain = prompt.pipe(model); - - const memory = { history: "Assistant: Hello! How can I assist you today?" }; - await chain.invoke( - { input: "What's your name?", ...memory }, - { callbacks: [handler], tags: ["test"] }, - ); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - debugger; - - expect(spans).toMatchObject([ - { - span_attributes: { - name: "RunnableSequence", - type: "task", - }, - input: { - history: "Assistant: Hello! How can I assist you today?", - input: "What's your name?", - }, - metadata: { - tags: ["test"], - }, - span_id: root_span_id, - root_span_id: trace_id, - }, - { - span_attributes: { name: "ChatPromptTemplate", type: "task" }, - input: { - history: "Assistant: Hello! How can I assist you today?", - input: "What's your name?", - }, - output: expect.objectContaining({ - kwargs: expect.objectContaining({ - messages: expect.arrayContaining([ - expect.objectContaining({ - kwargs: expect.objectContaining({ - content: - "Assistant: Hello! How can I assist you today? User: What's your name?", - }), - }), - ]), - }), - }), - metadata: { tags: ["seq:step:1", "test"] }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - { - span_attributes: { name: "ChatOpenAI", type: "llm" }, - input: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - kwargs: expect.objectContaining({ - content: - "Assistant: Hello! How can I assist you today? User: What's your name?", - }), - }), - ]), - ]), - output: expect.objectContaining({ - generations: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - message: expect.objectContaining({ - kwargs: expect.objectContaining({ - content: expect.stringContaining("Assistant"), - }), - }), - }), - ]), - ]), - }), - metrics: { - completion_tokens: expect.any(Number), - end: expect.any(Number), - prompt_tokens: expect.any(Number), - start: expect.any(Number), - tokens: expect.any(Number), - }, - metadata: { - tags: ["seq:step:2", "test"], - model: "gpt-4o-mini-2024-07-18", - }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - ]); -}); - -it("should handle tool/agent usage", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post("https://api.openai.com/v1/chat/completions", async () => { - return HttpResponse.json(CHAT_TOOL_CALCULATOR); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["tool-span-id"]); - }), - ); - - const llm = new ChatOpenAI({ - model: "gpt-4o-mini-2024-07-18", - }); - - const calculatorSchema = z.object({ - operation: z - .enum(["add", "subtract", "multiply", "divide"]) - .describe("The type of operation to execute."), - number1: z.number().describe("The first number to operate on."), - number2: z.number().describe("The second number to operate on."), - }); - - const calculatorTool = tool( - ({ operation, number1, number2 }) => { - // Functions must return strings - if (operation === "add") { - return `${number1 + number2}`; - } else if (operation === "subtract") { - return `${number1 - number2}`; - } else if (operation === "multiply") { - return `${number1 * number2}`; - } else if (operation === "divide") { - return `${number1 / number2}`; - } else { - throw new Error("Invalid operation."); - } - }, - { - name: "calculator", - description: "Can perform mathematical operations.", - schema: calculatorSchema, - }, - ); - - const llmWithTools = llm.bindTools([calculatorTool]); - - await llmWithTools.invoke("What is 3 * 12", { - callbacks: [handler], - }); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - debugger; - - expect(spans).toMatchObject([ - { - span_attributes: { - name: "ChatOpenAI", - type: "llm", - }, - input: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - kwargs: expect.objectContaining({ - content: "What is 3 * 12", - }), - }), - ]), - ]), - output: expect.objectContaining({ - generations: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - message: expect.objectContaining({ - kwargs: expect.objectContaining({ - tool_calls: expect.arrayContaining([ - expect.objectContaining({ - name: "calculator", - args: { - operation: "multiply", - number1: 3, - number2: 12, - }, - }), - ]), - }), - }), - }), - ]), - ]), - }), - metrics: { - completion_tokens: expect.any(Number), - end: expect.any(Number), - prompt_tokens: expect.any(Number), - start: expect.any(Number), - tokens: expect.any(Number), - }, - metadata: { - model: "gpt-4o-mini-2024-07-18", - tags: [], - }, - span_id: root_span_id, - root_span_id: trace_id, - }, - ]); -}); - -it("should handle parallel runnable execution", async () => { - const logs: LogsRequest[] = []; - - const calls = [ - HttpResponse.json(CHAT_BEAR_JOKE), - HttpResponse.json(CHAT_BEAR_POEM), - ]; - - server.use( - http.post("https://api.openai.com/v1/chat/completions", () => { - return calls.shift() || HttpResponse.json({ ok: false }, { status: 500 }); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["parallel-span-id"]); - }), - ); - - const model = new ChatOpenAI({ model: "gpt-4o-mini-2024-07-18" }); - - const jokeChain = PromptTemplate.fromTemplate( - "Tell me a joke about {topic}", - ).pipe(model); - const poemChain = PromptTemplate.fromTemplate( - "write a 2-line poem about {topic}", - ).pipe(model); - - const mapChain = RunnableMap.from({ - joke: jokeChain, - poem: poemChain, - }); - - await mapChain.invoke({ topic: "bear" }, { callbacks: [handler] }); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - // Check that we have the expected structure - expect(spans).toMatchObject([ - { - span_attributes: { - name: "RunnableMap", - type: "task", - }, - input: { - input: { - topic: "bear", - }, - }, - metadata: { - tags: [], - }, - span_id: root_span_id, - root_span_id: trace_id, - }, - { - // Updated for LangGraph 1.x API using Annotation - span_attributes: { name: "RunnableSequence", type: "task" }, - input: { topic: "bear" }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - { - span_attributes: { name: "RunnableSequence", type: "task" }, - input: { topic: "bear" }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - // Additional spans for the prompts and models - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - ]); -}); - -it("should handle LangGraph state management", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post("https://api.openai.com/v1/chat/completions", async () => { - return HttpResponse.json(CHAT_SAY_HELLO); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["graph-span-id"]); - }), - ); - - const GraphState = Annotation.Root({ - message: Annotation({ - reducer: (_, y) => y, - default: () => "", - }), - }); - - type State = typeof GraphState.State; - type StateUpdate = typeof GraphState.Update; - - const model = new ChatOpenAI({ - model: "gpt-4o-mini-2024-07-18", - callbacks: [handler], - }); - - async function sayHello(_: State): Promise { - const res = await model.invoke("Say hello"); - return { message: typeof res.content === "string" ? res.content : "" }; - } - - function sayBye(_: State): StateUpdate { - console.log(`From the 'sayBye' node: Bye world!`); - return {}; - } - - const graphBuilder = new StateGraph(GraphState) // Add our nodes to the Graph - .addNode("sayHello", sayHello) - .addNode("sayBye", sayBye) // Add the edges between nodes - .addEdge(START, "sayHello") - .addEdge("sayHello", "sayBye") - .addEdge("sayBye", END); - - const helloWorldGraph = graphBuilder.compile(); - - await helloWorldGraph.invoke({}, { callbacks: [handler] }); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - expect(spans).toMatchObject([ - { - span_attributes: { - name: "LangGraph", - type: "task", - }, - input: {}, - span_id: root_span_id, - root_span_id: trace_id, - }, - { - span_attributes: { name: "sayHello", type: "task" }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - { - span_attributes: { name: "ChatOpenAI", type: "llm" }, - input: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - kwargs: expect.objectContaining({ - content: "Say hello", - }), - }), - ]), - ]), - output: expect.objectContaining({ - generations: expect.anything(), - }), - root_span_id: trace_id, - span_parents: expect.anything(), - }, - { - span_attributes: { name: "sayBye", type: "task" }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - ]); -}); - -it("should have correctly typed constructor parameters", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post("https://api.openai.com/v1/chat/completions", () => { - return HttpResponse.json(CHAT_MATH); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["4bc6305f-2175-4481-bc84-7c55a456b7ea"]); - }), - ); - - const handler = new BraintrustCallbackHandler({ - logger: NOOP_SPAN, - }); - - handler.handleLLMStart( - { - name: "test", - lc: 1, - type: "secret", - id: ["test"], - }, - ["test"], - "test", - "test", - ); - - await flush(); - - expect(logs).toEqual([]); -}); - -it("should handle chain inputs/outputs with null/undefined values", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["null-span-id"]); - }), - ); - - // Test chain with null/undefined inputs - await handler.handleChainStart( - { id: ["TestChain"], lc: 1, type: "not_implemented" }, - { input1: "value1", input2: null, input3: undefined }, - "run-1", - undefined, - ["test"], - ); - - await handler.handleChainEnd( - { output1: "value1", output2: null, output3: undefined }, - "run-1", - undefined, - ["test"], - ); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - expect(spans).toMatchObject([ - { - span_attributes: { - name: "TestChain", - type: "task", - }, - input: { - input1: "value1", - input2: null, - }, - output: { - output1: "value1", - output2: null, - }, - metadata: { - tags: ["test"], - }, - span_id: root_span_id, - root_span_id: trace_id, - }, - ]); -}); - -it("should handle agent action and agent end callbacks", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["agent-span-id"]); - }), - ); - - const agentAction = { - tool: "calculator", - toolInput: { operation: "multiply", a: 3, b: 12 }, - log: "Invoking calculator with multiply 3 * 12", - }; - - const agentFinish = { - returnValues: { output: "The result is 36" }, - log: "Final answer: 36", - }; - - await handler.handleAgentAction(agentAction, "agent-run-1", undefined, [ - "agent-test", - ]); - - await handler.handleAgentEnd(agentFinish, "agent-run-1", undefined, [ - "agent-test", - ]); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - expect(spans).toMatchObject([ - { - span_attributes: { - name: "calculator", - type: "llm", - }, - input: { - tool: "calculator", - toolInput: { operation: "multiply", a: 3, b: 12 }, - log: "Invoking calculator with multiply 3 * 12", - }, - output: { - returnValues: { output: "The result is 36" }, - log: "Final answer: 36", - }, - metadata: { - tags: ["agent-test"], - }, - span_id: root_span_id, - root_span_id: trace_id, - }, - ]); -}); - -it("should handle agent action with string toolInput", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["agent-string-span-id"]); - }), - ); - - const agentAction = { - tool: "search", - toolInput: "What is the weather in Paris?", - log: "Searching for weather information", - }; - - const agentFinish = { - returnValues: { output: "It is sunny and 22 degrees in Paris" }, - log: "Search complete", - }; - - await handler.handleAgentAction(agentAction, "agent-run-2", undefined, [ - "search-test", - ]); - - await handler.handleAgentEnd(agentFinish, "agent-run-2", undefined, [ - "search-test", - ]); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - expect(spans).toMatchObject([ - { - span_attributes: { - name: "search", - type: "llm", - }, - input: { - tool: "search", - toolInput: "What is the weather in Paris?", - log: "Searching for weather information", - }, - output: { - returnValues: { output: "It is sunny and 22 degrees in Paris" }, - log: "Search complete", - }, - metadata: { - tags: ["search-test"], - }, - span_id: root_span_id, - root_span_id: trace_id, - }, - ]); -}); - -it("should handle nested agent action with parent run id", async () => { - const logs: LogsRequest[] = []; - - server.use( - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["nested-agent-span-id"]); - }), - ); - - await handler.handleChainStart( - { id: ["AgentExecutor"], lc: 1, type: "not_implemented" }, - { input: "Calculate 3 * 12" }, - "parent-run-1", - undefined, - ["agent-executor"], - ); - - const agentAction = { - tool: "calculator", - toolInput: { a: 3, b: 12, operation: "multiply" }, - log: "Using calculator tool", - }; - - await handler.handleAgentAction( - agentAction, - "child-agent-run", - "parent-run-1", - ["agent-action"], - ); - - const agentFinish = { - returnValues: { output: "36" }, - log: "Calculation complete", - }; - - await handler.handleAgentEnd(agentFinish, "child-agent-run", "parent-run-1", [ - "agent-action", - ]); - - await handler.handleChainEnd( - { output: "The answer is 36" }, - "parent-run-1", - undefined, - ["agent-executor"], - ); - - await flush(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - expect(spans.length).toBe(2); - - expect(spans[0]).toMatchObject({ - span_attributes: { - name: "AgentExecutor", - type: "task", - }, - input: { input: "Calculate 3 * 12" }, - output: { output: "The answer is 36" }, - metadata: { - tags: ["agent-executor"], - }, - span_id: root_span_id, - root_span_id: trace_id, - }); - - expect(spans[1]).toMatchObject({ - span_attributes: { - name: "calculator", - type: "llm", - }, - input: { - tool: "calculator", - toolInput: { a: 3, b: 12, operation: "multiply" }, - log: "Using calculator tool", - }, - output: { - returnValues: { output: "36" }, - log: "Calculation complete", - }, - metadata: { - tags: ["agent-action"], - }, - root_span_id: trace_id, - span_parents: [root_span_id], - }); -}); - -it("should extract prompt caching tokens from Anthropic response", async () => { - const logs: LogsRequest[] = []; - - const ANTHROPIC_RESPONSE_WITH_CACHE = { - model: "claude-sonnet-4-5-20250929", - id: "msg_01PKL7azhWzdzdXC5yojHG6V", - type: "message", - role: "assistant", - content: [ - { - type: "text", - text: 'The first type of testing mentioned is **Unit Testing**, which is described as "Testing individual components or functions in isolation."', - }, - ], - stop_reason: "end_turn", - stop_sequence: null, - usage: { - input_tokens: 14, - cache_creation_input_tokens: 2042, - cache_read_input_tokens: 0, - cache_creation: { - ephemeral_5m_input_tokens: 0, - ephemeral_1h_input_tokens: 0, - }, - output_tokens: 27, - service_tier: "standard", - }, - }; - - const ANTHROPIC_RESPONSE_WITH_CACHE_READ = { - model: "claude-sonnet-4-5-20250929", - id: "msg_01PjHqPkFstyG2Bcqum9XKD8", - type: "message", - role: "assistant", - content: [ - { - type: "text", - text: 'Based on the document:\n\n1. **First type of testing mentioned:** Unit Testing - described as "Testing individual components or functions in isolation"\n\n2. **Python testing frameworks mentioned:** \n - pytest (described as "Feature-rich testing framework")\n - unittest (described as "Built-in Python testing module")', - }, - ], - stop_reason: "end_turn", - stop_sequence: null, - usage: { - input_tokens: 23, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 2042, - cache_creation: { - ephemeral_5m_input_tokens: 0, - ephemeral_1h_input_tokens: 0, - }, - output_tokens: 71, - service_tier: "standard", - }, - }; - - const anthropicResponses = [ - ANTHROPIC_RESPONSE_WITH_CACHE, - ANTHROPIC_RESPONSE_WITH_CACHE_READ, - ]; - - server.use( - http.post("https://api.anthropic.com/v1/messages", ({ request }) => { - const message = anthropicResponses.shift(); - return HttpResponse.json(message); - }), - - http.post(/.+logs.+/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - console.log(logs.slice(-1)[0]); - return HttpResponse.json(["cache-tokens-span-id"]); - }), - ); - - const model = new ChatAnthropic({ - model: "claude-sonnet-4-5-20250929", - anthropicApiKey: "test-key", - }); - - // Use a long system message to simulate cache-eligible content - const longText = ` -# Comprehensive Guide to Software Testing Methods - -Software testing is a critical component of the software development lifecycle. -This guide covers various testing methodologies, best practices, and tools. - -## Types of Testing -- Unit Testing: Testing individual components or functions in isolation -- Integration Testing: Testing how components work together -- End-to-End Testing: Testing the entire application flow - -## Python Testing Tools -- pytest: Feature-rich testing framework -- unittest: Built-in Python testing module -`.repeat(20); // Repeat to ensure enough tokens for caching - - const messages = [ - new SystemMessage({ - content: [ - { - type: "text", - text: longText, - cache_control: { type: "ephemeral" }, - }, - ], - }), - new HumanMessage("What is the first type of testing mentioned?"), - ]; - - // First invocation - should create cache - await model.invoke(messages, { callbacks: [handler] }); - - await flush(); - - const { spans: firstSpans } = logsToSpans(logs); - logs.length = 0; // Clear logs for next invocation - - const firstLlmSpan = firstSpans.find( - (s) => s.span_attributes?.type === "llm", - ); - - expect(firstLlmSpan).toBeDefined(); - expect(firstLlmSpan?.metrics).toMatchObject({ - prompt_tokens: 14, - completion_tokens: 27, - }); - - console.log(firstLlmSpan?.metrics); - - // After fix: should have cache creation tokens - expect(firstLlmSpan?.metrics?.prompt_cache_creation_tokens).toBe(2042); - - // Second invocation - should read from cache - await model.invoke( - [ - ...messages, - new HumanMessage("What testing framework is mentioned for Python?"), - ], - { callbacks: [handler] }, - ); - - await flush(); - - const { spans: secondSpans } = logsToSpans(logs); - - const secondLlmSpan = secondSpans.find( - (s) => s.span_attributes?.type === "llm", - ); - - expect(secondLlmSpan).toBeDefined(); - expect(secondLlmSpan?.metrics).toMatchObject({ - prompt_tokens: 23, - completion_tokens: 71, - }); - - // After fix: should have cache read tokens - expect(secondLlmSpan?.metrics?.prompt_cached_tokens).toBe(2042); -}); diff --git a/integrations/langchain-js/src/BraintrustCallbackHandler.ts b/integrations/langchain-js/src/BraintrustCallbackHandler.ts deleted file mode 100644 index 9719a972f..000000000 --- a/integrations/langchain-js/src/BraintrustCallbackHandler.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; -import { AgentAction, AgentFinish } from "@langchain/core/dist/agents"; -import { DocumentInterface } from "@langchain/core/dist/documents/document"; -import { Serialized } from "@langchain/core/dist/load/serializable"; -import { BaseMessage } from "@langchain/core/dist/messages/base"; -import { ChatResult, LLMResult } from "@langchain/core/dist/outputs"; -import { ChainValues } from "@langchain/core/dist/utils/types"; -import { RunnableConfig } from "@langchain/core/runnables"; -import { - BraintrustLangChainCallbackHandler, - type LangChainCallbackHandlerOptions, -} from "braintrust"; - -type BraintrustCallbackHandlerOptions = LangChainCallbackHandlerOptions; - -/** - * A Braintrust tracer for LangChain.js that logs LLM calls, chains, and tools. - * - * @deprecated Use automatic LangChain instrumentation from `braintrust`, or - * `BraintrustLangChainCallbackHandler` from `braintrust` for manual callbacks. - * This package will stop being published after the next release. - */ -export class BraintrustCallbackHandler extends BaseCallbackHandler { - name = "BraintrustCallbackHandler"; - private inner: BraintrustLangChainCallbackHandler; - - constructor(options?: Partial) { - super(); - this.inner = new BraintrustLangChainCallbackHandler(options); - } - - handleLLMStart( - llm: Serialized, - prompts: string[], - runId: string, - parentRunId?: string, - extraParams?: { - options: RunnableConfig; - invocation_params?: Record; - batch_size: number; - cache?: boolean; - }, - tags?: string[], - metadata?: Record, - runName?: string, - ): Promise { - return this.inner.handleLLMStart( - llm, - prompts, - runId, - parentRunId, - extraParams, - tags, - metadata, - runName, - ); - } - - handleLLMError( - err: Error, - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleLLMError(err, runId, parentRunId, tags); - } - - handleLLMEnd( - output: LLMResult | ChatResult, - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleLLMEnd(output, runId, parentRunId, tags); - } - - handleLLMNewToken( - token: string, - idx: { prompt: number; completion: number }, - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleLLMNewToken(token, idx, runId, parentRunId, tags); - } - - handleChatModelStart( - llm: Serialized, - messages: BaseMessage[][], - runId: string, - parentRunId?: string, - extraParams?: { - options: RunnableConfig; - invocation_params?: Record; - batch_size: number; - cache?: boolean; - }, - tags?: string[], - metadata?: Record, - runName?: string, - ): Promise { - return this.inner.handleChatModelStart( - llm, - messages, - runId, - parentRunId, - extraParams, - tags, - metadata, - runName, - ); - } - - handleChainStart( - chain: Serialized, - inputs: ChainValues, - runId: string, - parentRunId?: string, - tags?: string[], - metadata?: Record, - runType?: string, - runName?: string, - ): Promise { - return this.inner.handleChainStart( - chain, - inputs, - runId, - parentRunId, - tags, - metadata, - runType, - runName, - ); - } - - handleChainError( - err: Error, - runId: string, - parentRunId?: string, - tags?: string[], - kwargs?: { inputs?: Record }, - ): Promise { - return this.inner.handleChainError(err, runId, parentRunId, tags, kwargs); - } - - handleChainEnd( - outputs: ChainValues, - runId: string, - parentRunId?: string, - tags?: string[], - kwargs?: { inputs?: Record }, - ): Promise { - return this.inner.handleChainEnd(outputs, runId, parentRunId, tags, kwargs); - } - - handleToolStart( - tool: Serialized, - input: string, - runId: string, - parentRunId?: string, - tags?: string[], - metadata?: Record, - runName?: string, - ): Promise { - return this.inner.handleToolStart( - tool, - input, - runId, - parentRunId, - tags, - metadata, - runName, - ); - } - - handleToolError( - err: Error, - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleToolError(err, runId, parentRunId, tags); - } - - handleToolEnd( - output: unknown, - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleToolEnd(output, runId, parentRunId, tags); - } - - handleAgentAction( - action: AgentAction, - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleAgentAction( - action as unknown as Record, - runId, - parentRunId, - tags, - ); - } - - handleAgentEnd( - action: AgentFinish, - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleAgentEnd(action, runId, parentRunId, tags); - } - - handleRetrieverStart( - retriever: Serialized, - query: string, - runId: string, - parentRunId?: string, - tags?: string[], - metadata?: Record, - name?: string, - ): Promise { - return this.inner.handleRetrieverStart( - retriever, - query, - runId, - parentRunId, - tags, - metadata, - name, - ); - } - - handleRetrieverEnd( - documents: DocumentInterface[], - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleRetrieverEnd(documents, runId, parentRunId, tags); - } - - handleRetrieverError( - err: Error, - runId: string, - parentRunId?: string, - tags?: string[], - ): Promise { - return this.inner.handleRetrieverError(err, runId, parentRunId, tags); - } -} diff --git a/integrations/langchain-js/src/index.ts b/integrations/langchain-js/src/index.ts deleted file mode 100644 index ad7af8003..000000000 --- a/integrations/langchain-js/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BraintrustCallbackHandler } from "./BraintrustCallbackHandler"; -export { setGlobalHandler } from "./setGlobalHandler"; diff --git a/integrations/langchain-js/src/setGlobalHandler.test.ts b/integrations/langchain-js/src/setGlobalHandler.test.ts deleted file mode 100644 index 42cdffb96..000000000 --- a/integrations/langchain-js/src/setGlobalHandler.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { CallbackManager } from "@langchain/core/callbacks/manager"; -import { ChatPromptTemplate } from "@langchain/core/prompts"; -import { ChatOpenAI } from "@langchain/openai"; -import { flush as flushBraintrustLogs } from "braintrust"; -import { http, HttpResponse } from "msw"; -import { afterEach, describe, expect, it } from "vitest"; -import { BraintrustCallbackHandler } from "./BraintrustCallbackHandler"; -import { CHAT_MATH } from "./BraintrustCallbackHandler.fixtures"; -import { clearGlobalHandler, setGlobalHandler } from "./setGlobalHandler"; -import { server } from "./test/setup"; -import { LogsRequest } from "./test/types"; -import { logsToSpans, withLogging } from "./test/utils"; - -const handler = withLogging(new BraintrustCallbackHandler({ debug: true })); - -describe("setGlobalHandler", () => { - afterEach(() => { - clearGlobalHandler(); - }); - - it("should register the BraintrustCallbackHandler", async () => { - setGlobalHandler(handler); - - // Make sure the handler is registered in the LangChain.js library. - const manager = CallbackManager.configure(); - expect( - manager?.handlers.filter( - (handler) => handler instanceof BraintrustCallbackHandler, - )[0], - ).toBe(handler); - - const logs: LogsRequest[] = []; - - // Intercept calls to confirm our tracing is working. - server.use( - http.post("https://api.openai.com/v1/chat/completions", () => { - return HttpResponse.json(CHAT_MATH); - }), - - http.post(/.+logs/, async ({ request }) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - logs.push((await request.json()) as LogsRequest); - return HttpResponse.json(["4bc6305f-2175-4481-bc84-7c55a456b7ea"]); - }), - ); - - // Here's what a typical user would do. - const prompt = ChatPromptTemplate.fromTemplate(`What is 1 + {number}?`); - const model = new ChatOpenAI({ - model: "gpt-4o-mini-2024-07-18", - }); - - const chain = prompt.pipe(model); - - const message = await chain.invoke({ number: "2" }); - - // Not normally needed by users, but we need it for our tests. - await flushBraintrustLogs(); - - const { spans, trace_id, root_span_id } = logsToSpans(logs); - - // Spans would be empty if the handler was not registered, let's make sure it logged what we expect. - expect(spans).toMatchObject([ - { - span_attributes: { - name: "RunnableSequence", - type: "task", - }, - input: { - number: "2", - }, - metadata: { - tags: [], - }, - span_id: root_span_id, - root_span_id: trace_id, - }, - { - span_attributes: { name: "ChatPromptTemplate", type: "task" }, - input: { number: "2" }, - output: expect.objectContaining({ - kwargs: expect.objectContaining({ - messages: expect.arrayContaining([ - expect.objectContaining({ - kwargs: expect.objectContaining({ - content: "What is 1 + 2?", - }), - }), - ]), - }), - }), - metadata: { tags: ["seq:step:1"] }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - { - span_attributes: { name: "ChatOpenAI", type: "llm" }, - input: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - kwargs: expect.objectContaining({ - content: "What is 1 + 2?", - }), - }), - ]), - ]), - output: expect.objectContaining({ - generations: expect.arrayContaining([ - expect.arrayContaining([ - expect.objectContaining({ - message: expect.objectContaining({ - kwargs: expect.objectContaining({ - content: "1 + 2 equals 3.", - }), - }), - }), - ]), - ]), - }), - metrics: { - completion_tokens: 8, - end: expect.any(Number), - prompt_tokens: 15, - start: expect.any(Number), - tokens: 23, - }, - metadata: { - tags: ["seq:step:2"], - model: "gpt-4o-mini-2024-07-18", - }, - root_span_id: trace_id, - span_parents: [root_span_id], - }, - ]); - - expect(message.content).toBe("1 + 2 equals 3."); - }); -}); diff --git a/integrations/langchain-js/src/setGlobalHandler.ts b/integrations/langchain-js/src/setGlobalHandler.ts deleted file mode 100644 index e109d5e91..000000000 --- a/integrations/langchain-js/src/setGlobalHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; -import { - registerConfigureHook, - setContextVariable, -} from "@langchain/core/context"; - -const BT_HANDLER = "BT_HANDLER"; - -/** - * @deprecated Use automatic LangChain instrumentation from `braintrust` for - * application-wide coverage. This package will stop being published after the - * next release. - */ -export const setGlobalHandler = (handler: BaseCallbackHandler) => { - setContextVariable(BT_HANDLER, handler); - - registerConfigureHook({ - contextVar: BT_HANDLER, - }); -}; - -export const clearGlobalHandler = () => { - setContextVariable(BT_HANDLER, undefined); -}; diff --git a/integrations/langchain-js/src/test/setup.ts b/integrations/langchain-js/src/test/setup.ts deleted file mode 100644 index 440a1435b..000000000 --- a/integrations/langchain-js/src/test/setup.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { http, HttpResponse } from "msw"; -import { setupServer } from "msw/node"; -import { afterAll, afterEach, beforeAll } from "vitest"; - -export const server = setupServer( - http.post(/.+\/api\/apikey\/login/, () => { - return HttpResponse.json({ - org_info: [ - { - id: "abb9f3e4-7fdd-4ccc-af40-f7e894fd4125", - name: "braintrustdata.com", - api_url: "http://0.0.0.0:8000", - git_metadata: null, - is_universal_api: null, - proxy_url: "http://0.0.0.0:8000", - realtime_url: "ws://0.0.0.0:8788", - }, - ], - }); - }), - - http.post(/.+\/api\/project\/register/, () => { - return HttpResponse.json({ - project: { - id: "209220fc-d3bd-4fab-b1a4-af5827d69200", - org_id: "abb9f3e4-7fdd-4ccc-af40-f7e894fd4125", - name: "Global", - created: "2024-12-04T16:14:11.122Z", - deleted_at: null, - user_id: "fac36c53-c882-458b-bf80-60d06c3e8a0d", - settings: null, - }, - }); - }), -); - -beforeAll(() => { - process.env.BRAINTRUST_API_KEY = "braintrust-api-key"; - process.env.BRAINTRUST_APP_URL = "http://braintrust.local"; - process.env.BRAINTRUST_ORG_NAME = "braintrustdata.com"; - process.env.OPENAI_API_KEY = "openai-api-key"; - - server.listen({ - onUnhandledRequest: (req) => { - throw new Error(`Unhandled request ${req.method}, ${req.url}`); - }, - }); -}); - -afterEach(() => server.resetHandlers()); -afterAll(() => server.close()); diff --git a/integrations/langchain-js/src/test/types.ts b/integrations/langchain-js/src/test/types.ts deleted file mode 100644 index 10b42c2df..000000000 --- a/integrations/langchain-js/src/test/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -type SpanAttributes = { - name: string; - exec_counter: number; - type?: string; -}; - -type Metadata = { - tags: string[]; - params: Record; - [key: string]: unknown; -}; - -type Output = { - parsed: string; - raw: Record; -}; - -type LogRow = { - span_attributes?: SpanAttributes; - input?: Record; - metadata?: Metadata; - output?: Output; - metrics?: Record; - span_id: string; - root_span_id: string; - span_parents: string[]; -}; - -export type LogsRequest = { - rows: LogRow[]; - api_version: number; -}; diff --git a/integrations/langchain-js/src/test/utils.ts b/integrations/langchain-js/src/test/utils.ts deleted file mode 100644 index 52761197c..000000000 --- a/integrations/langchain-js/src/test/utils.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { mergeDicts } from "../../../../js/util/object_util"; -import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; -import { LogsRequest } from "./types"; - -export const logsToSpans = (logs: LogsRequest[]) => { - if (logs.length === 0) { - throw new Error("No logs to convert to spans"); - } - - // Logs include partial updates (merges) for previous rows. - // We need to dedupe these and merge them. So we can see the final state like - // we do in the UI. - - const seenIds = new Set(); - const spans = logs - .flatMap((log) => log.rows) - .reduce( - (acc, row) => { - if (!seenIds.has(row.span_id)) { - seenIds.add(row.span_id); - acc.push(row); - } else { - const existingSpan = acc.find((span) => span.span_id === row.span_id); - if (existingSpan) { - mergeDicts(existingSpan, row); - } - } - return acc; - }, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - [] as LogsRequest["rows"], - ); - - return { - spans, - trace_id: spans[0].root_span_id, - root_span_id: spans[0].span_id, - root_run_id: spans[0].metadata?.runId, - }; -}; - -export const withLogging = (handler: T): T => { - if (process.env.VERBOSE === "false") { - return handler; - } - - let depth = 0; - return new Proxy(handler, { - get(target, prop) { - // @ts-expect-error - const originalMethod = target[prop]; - if (typeof originalMethod === "function") { - // @ts-expect-error - return function (...args) { - const propString = String(prop); - if (!["startSpan", "endSpan"].includes(propString)) { - console.log(new Array(depth * 2).join(" ") + propString); - - if (propString.endsWith("Start")) { - depth++; - } else if (propString.endsWith("End")) { - depth = Math.max(0, depth - 1); - } - } - // @ts-expect-error - return originalMethod.apply(this, args); - }; - } - return originalMethod; - }, - }); -}; diff --git a/integrations/langchain-js/tsconfig.json b/integrations/langchain-js/tsconfig.json deleted file mode 100644 index ea6ad1861..000000000 --- a/integrations/langchain-js/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "lib": ["es2022"], - "module": "commonjs", - "target": "es2022", - "moduleResolution": "node", - "strict": true, - "noUnusedLocals": true, - "esModuleInterop": true, - "skipLibCheck": true - }, - "include": ["."], - "exclude": ["node_modules/**", "**/dist/**"] -} diff --git a/integrations/langchain-js/tsup.config.ts b/integrations/langchain-js/tsup.config.ts deleted file mode 100644 index 4c5a7bd9b..000000000 --- a/integrations/langchain-js/tsup.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig([ - { - entry: ["src/index.ts"], - format: ["cjs", "esm"], - outDir: "dist", - external: ["braintrust", "@langchain/core", "@langchain/langgraph"], - dts: true, - }, -]); diff --git a/integrations/langchain-js/turbo.json b/integrations/langchain-js/turbo.json deleted file mode 100644 index 3e50dc890..000000000 --- a/integrations/langchain-js/turbo.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": ["//"], - "tasks": { - "build": { - "outputs": ["**/dist/**"] - } - } -} diff --git a/integrations/langchain-js/vitest.config.ts b/integrations/langchain-js/vitest.config.ts deleted file mode 100644 index 9c517c86c..000000000 --- a/integrations/langchain-js/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - reporters: ["default"], - setupFiles: ["./src/test/setup.ts"], - include: ["tests/**/*.test.ts", "src/**/*.test.ts"], - }, -}); diff --git a/integrations/openai-agents-js/CHANGELOG.md b/integrations/openai-agents-js/CHANGELOG.md deleted file mode 100644 index 39db538e6..000000000 --- a/integrations/openai-agents-js/CHANGELOG.md +++ /dev/null @@ -1,20 +0,0 @@ -# @braintrust/openai-agents - -## 0.1.6 - -- `@braintrust/openai-agents` is now deprecated. This release marks the last release for this package. (https://github.com/braintrustdata/braintrust-sdk-javascript/pull/2383) -- Updated dependencies: braintrust@3.29.0 - -### Migration - -To migrate, install `braintrust` and import `OpenAIAgentsTraceProcessor` from -`braintrust/instrumentation` instead. See the -[OpenAI Agents SDK integration guide](https://www.braintrust.dev/docs/integrations/agent-frameworks/openai-agents-sdk) -for setup examples. - -## 0.1.5 - -### Patch Changes - -- Ensured the root span is flushed during `onTraceEnd()` so traces are marked complete even in short-lived and serverless processes. -- Added test coverage around trace completion and root span flushing behavior. diff --git a/integrations/openai-agents-js/README.md b/integrations/openai-agents-js/README.md deleted file mode 100644 index fe0e3b011..000000000 --- a/integrations/openai-agents-js/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# @braintrust/openai-agents - -[![npm version](https://img.shields.io/npm/v/%40braintrust%2Fopenai-agents.svg)](https://www.npmjs.com/package/@braintrust/openai-agents) - -SDK for integrating Braintrust with OpenAI Agents. - -> [!WARNING] -> This package is deprecated and will stop being published after the next -> release. Use the OpenAI Agents integration in `braintrust` instead. - -## Installation - -```bash -npm install braintrust @openai/agents -# or -yarn add braintrust @openai/agents -# or -pnpm add braintrust @openai/agents -``` - -## Requirements - -- `braintrust` -- `@openai/agents` - -## Quickstart - -```typescript -import { initLogger } from "braintrust"; -import { OpenAIAgentsTraceProcessor } from "braintrust/instrumentation"; -import { Agent, run, addTraceProcessor } from "@openai/agents"; - -// Initialize Braintrust logger -const logger = initLogger({ - projectName: "my-agents-project", -}); - -// Create the tracing processor -const processor = new OpenAIAgentsTraceProcessor({ logger }); - -// Add the processor to OpenAI Agents -addTraceProcessor(processor); - -// Create and run your agent -const agent = new Agent({ - name: "my-agent", - model: "gpt-4o-mini", - instructions: "You are a helpful assistant.", -}); - -const result = await run(agent, "Hello, world!"); -``` - -## API - -### `OpenAIAgentsTraceProcessor` - -A tracing processor that logs traces from the OpenAI Agents SDK to Braintrust. - -#### Constructor - -```typescript -new OpenAIAgentsTraceProcessor(options?: OpenAIAgentsTraceProcessorOptions) -``` - -**Options:** - -- `logger?: Logger` - A Braintrust `Span`, `Experiment`, or `Logger` to use for logging. If undefined, the current span, experiment, or logger will be selected exactly as in `startSpan`. -- `maxTraces?: number` - Maximum number of concurrent traces to keep in memory (default: 10000). When exceeded, oldest traces are evicted using LRU policy to prevent memory leaks. - -## Documentation - -- Braintrust docs: [https://www.braintrust.dev/docs](https://www.braintrust.dev/docs) -- OpenAI Agents docs: [https://openai.github.io/openai-agents-js/](https://openai.github.io/openai-agents-js/) diff --git a/integrations/openai-agents-js/package.json b/integrations/openai-agents-js/package.json deleted file mode 100644 index f9f57ea10..000000000 --- a/integrations/openai-agents-js/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@braintrust/openai-agents", - "version": "0.1.6", - "description": "SDK for integrating Braintrust with OpenAI Agents", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "scripts": { - "build": "tsup", - "check:typings": "tsc --noEmit", - "watch": "tsup --watch", - "clean": "rm -r dist/*", - "test": "vitest run", - "yalc:publish": "yalc publish" - }, - "author": "Braintrust Data Inc.", - "license": "MIT", - "devDependencies": { - "@openai/agents": "^0.0.15", - "@types/node": "^20.10.5", - "braintrust": "workspace:^", - "tsup": "^8.5.1", - "typescript": "^5.3.3", - "vitest": "4.1.5", - "zod": "^3.25.34" - }, - "peerDependencies": { - "braintrust": ">=0.4.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", - "directory": "integrations/openai-agents-js" - }, - "homepage": "https://www.braintrust.dev/docs", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/", - "provenance": true - } -} diff --git a/integrations/openai-agents-js/src/index.ts b/integrations/openai-agents-js/src/index.ts deleted file mode 100644 index 337beec3f..000000000 --- a/integrations/openai-agents-js/src/index.ts +++ /dev/null @@ -1,675 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - Span as BraintrustSpan, - startSpan, - Logger, - currentSpan, - NOOP_SPAN, - Attachment, - type StartSpanArgs, -} from "braintrust"; -import { - SpanType, - AgentsTrace, - AgentsSpan, - SpanInput, - SpanOutput, - TraceMetadata, - OpenAIAgentsTraceProcessorOptions, - isResponseSpanData, - isGenerationSpanData, - isAgentSpanData, - isFunctionSpanData, - isHandoffSpanData, - isGuardrailSpanData, - isCustomSpanData, - isMCPListToolsSpanData, - isTranscriptionSpanData, - isSpeechSpanData, - isSpeechGroupSpanData, -} from "./types"; - -function spanTypeFromAgents( - span: AgentsSpan, -): NonNullable { - const spanType = span.spanData.type; - - if ( - spanType === SpanType.AGENT || - spanType === SpanType.HANDOFF || - spanType === SpanType.CUSTOM || - spanType === SpanType.SPEECH_GROUP - ) { - return "task"; - } - - if ( - spanType === SpanType.FUNCTION || - spanType === SpanType.GUARDRAIL || - spanType === SpanType.MCP_TOOLS - ) { - return "tool"; - } - - if ( - spanType === SpanType.GENERATION || - spanType === SpanType.RESPONSE || - spanType === SpanType.TRANSCRIPTION || - spanType === SpanType.SPEECH - ) { - return "llm"; - } - - return "task"; -} - -function spanNameFromAgents(span: AgentsSpan): string { - const spanData = span.spanData; - - if ("name" in spanData && spanData.name) { - return spanData.name; - } - - switch (spanData.type) { - case SpanType.GENERATION: - return "Generation"; - case SpanType.RESPONSE: - return "Response"; - case SpanType.HANDOFF: - return "Handoff"; - case SpanType.MCP_TOOLS: - if (isMCPListToolsSpanData(spanData) && spanData.server) { - return `List Tools (${spanData.server})`; - } - return "MCP List Tools"; - case SpanType.TRANSCRIPTION: - return "Transcription"; - case SpanType.SPEECH: - return "Speech"; - case SpanType.SPEECH_GROUP: - return "Speech Group"; - case SpanType.AGENT: - case SpanType.FUNCTION: - case SpanType.GUARDRAIL: - case SpanType.CUSTOM: - return "name" in spanData && spanData.name ? spanData.name : "Unknown"; - default: - return "Unknown"; - } -} - -function getTimeElapsed(end?: string, start?: string): number | undefined { - if (!start || !end) return undefined; - const startTime = new Date(start).getTime(); - const endTime = new Date(end).getTime(); - if (isNaN(startTime) || isNaN(endTime)) return undefined; - return (endTime - startTime) / 1000; -} - -/** - * `OpenAIAgentsTraceProcessor` is a tracing processor that logs traces from the OpenAI Agents SDK to Braintrust. - * - * Args: - * options: Configuration options including: - * - logger: A `Span`, `Experiment`, or `Logger` to use for logging. - * If `undefined`, the current span, experiment, or logger will be selected exactly as in `startSpan`. - * - maxTraces: Maximum number of concurrent traces to keep in memory (default: 1000). - * When exceeded, oldest traces are evicted using LRU policy. - * - * @deprecated Import `OpenAIAgentsTraceProcessor` from `braintrust/instrumentation` instead. This package will stop being published after the next release. - * */ - -export class OpenAIAgentsTraceProcessor { - private static readonly DEFAULT_MAX_TRACES = 10000; - - private logger?: Logger; - private maxTraces: number; - private traceSpans = new Map< - string, - { - rootSpan: BraintrustSpan; - childSpans: Map; - metadata: TraceMetadata; - } - >(); - private traceOrder: string[] = []; - - // Expose for testing purposes - public readonly _traceSpans = this.traceSpans; - - constructor(options: OpenAIAgentsTraceProcessorOptions = {}) { - this.logger = options.logger; - this.maxTraces = - options.maxTraces ?? OpenAIAgentsTraceProcessor.DEFAULT_MAX_TRACES; - } - - private processInputImages(input: any): any { - if (Array.isArray(input)) { - return input.map((item) => this.processInputImages(item)); - } - - if (input && typeof input === "object") { - // Handle input_image type with base64 image data - if (input.type === "input_image" && typeof input.image === "string") { - let imageData = input.image; - - // Strip data URI prefix if present (e.g., "data:image/png;base64,") - const dataUriMatch = imageData.match(/^data:image\/(\w+);base64,(.*)$/); - let contentType = "image/png"; - let fileExtension = "png"; - - if (dataUriMatch) { - fileExtension = dataUriMatch[1]; - contentType = `image/${fileExtension}`; - imageData = dataUriMatch[2]; // Extract just the base64 part - } - - const filename = `input_image.${fileExtension}`; - - try { - // Convert base64 string to Blob - const binaryString = atob(imageData); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - const blob = new Blob([bytes], { type: contentType }); - - const attachment = new Attachment({ - data: blob, - filename: filename, - contentType: contentType, - }); - - return { - ...input, - image: attachment, - }; - } catch (e) { - console.error("Failed to process input image:", e); - console.error("Image data sample:", input.image.substring(0, 200)); - return input; - } - } - - // Recursively process nested objects - const result: any = {}; - for (const [key, value] of Object.entries(input)) { - result[key] = this.processInputImages(value); - } - return result; - } - - return input; - } - - private processOutputImages(output: any): any { - if (Array.isArray(output)) { - return output.map((item) => this.processOutputImages(item)); - } - - if (output && typeof output === "object") { - // Handle image_generation_call type - convert result to attachment - if (output.type === "image_generation_call" && output.result) { - let resultData = output.result; - - // Use output_format from the response - const fileExtension = output.output_format || "png"; - const contentType = `image/${fileExtension}`; - - // Strip data URI prefix if present (e.g., "data:image/png;base64,") - const dataUriMatch = resultData.match(/^data:image\/\w+;base64,(.*)$/); - if (dataUriMatch) { - resultData = dataUriMatch[1]; // Extract just the base64 part - } - - const baseFilename = - output.revised_prompt && typeof output.revised_prompt === "string" - ? output.revised_prompt.slice(0, 50).replace(/[^a-zA-Z0-9]/g, "_") - : "generated_image"; - const filename = `${baseFilename}.${fileExtension}`; - - try { - // Convert base64 string to Blob - const binaryString = atob(resultData); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - const blob = new Blob([bytes], { type: contentType }); - - const attachment = new Attachment({ - data: blob, - filename: filename, - contentType: contentType, - }); - - return { - ...output, - result: attachment, - }; - } catch (e) { - console.error("Failed to process output image:", e); - console.error("Result data sample:", output.result.substring(0, 200)); - return output; - } - } - - // Recursively process nested objects - const result: any = {}; - for (const [key, value] of Object.entries(output)) { - result[key] = this.processOutputImages(value); - } - return result; - } - - return output; - } - - private evictOldestTrace(): void { - if (this.traceOrder.length === 0) return; - - const oldestTraceId = this.traceOrder.shift()!; - this.traceSpans.delete(oldestTraceId); - } - - onTraceStart(trace: AgentsTrace): Promise { - if (this.traceOrder.length >= this.maxTraces) { - this.evictOldestTrace(); - } - - // Detect parent span from current execution context - let span: BraintrustSpan; - const current = currentSpan(); - - if (current && current !== NOOP_SPAN) { - // Create as child of current span - span = current.startSpan({ - name: trace.name, - type: "task", - }); - } else { - // No parent span available, create as root - span = this.logger - ? this.logger.startSpan({ - name: trace.name, - type: "task", - }) - : startSpan({ - name: trace.name, - type: "task", - }); - } - - span.log({ - input: "Agent workflow started", - metadata: { - group_id: trace.groupId, - ...(trace.metadata || {}), - }, - }); - - this.traceSpans.set(trace.traceId, { - rootSpan: span, - childSpans: new Map(), - metadata: { - firstInput: null, - lastOutput: null, - }, - }); - this.traceOrder.push(trace.traceId); - - return Promise.resolve(); - } - - async onTraceEnd(trace: AgentsTrace): Promise { - const traceData = this.traceSpans.get(trace.traceId); - - if (traceData) { - for (const [spanId, braintrustSpan] of traceData.childSpans) { - braintrustSpan.end(); - traceData.childSpans.delete(spanId); - } - - try { - traceData.rootSpan.log({ - input: traceData.metadata.firstInput, - output: traceData.metadata.lastOutput, - }); - traceData.rootSpan.end(); - await traceData.rootSpan.flush(); - } finally { - this.traceSpans.delete(trace.traceId); - const orderIndex = this.traceOrder.indexOf(trace.traceId); - if (orderIndex > -1) { - this.traceOrder.splice(orderIndex, 1); - } - } - } - } - - private extractAgentLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - if (!isAgentSpanData(spanData)) { - return {}; - } - - const data: Record = { - metadata: { - tools: spanData.tools, - handoffs: spanData.handoffs, - output_type: spanData.output_type, - }, - }; - - return data; - } - - private extractResponseLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - const data: Record = {}; - - if (!isResponseSpanData(spanData)) { - return data; - } - - if (spanData._input !== undefined) { - data.input = this.processInputImages(spanData._input); - } - - if (spanData._response !== undefined) { - data.output = this.processOutputImages(spanData._response.output); - } - - if (spanData._response) { - const { output, metadata, usage, output_text, ...otherFields } = - spanData._response; - data.metadata = otherFields; - } - - data.metrics = {}; - const ttft = getTimeElapsed( - span.endedAt ?? undefined, - span.startedAt ?? undefined, - ); - if (ttft !== undefined) { - data.metrics.time_to_first_token = ttft; - } - - let usage: any = null; - if (spanData._response?.usage) { - usage = spanData._response.usage; - } - - if (usage) { - if (usage.total_tokens) data.metrics.tokens = usage.total_tokens; - if (usage.input_tokens) data.metrics.prompt_tokens = usage.input_tokens; - if (usage.output_tokens) - data.metrics.completion_tokens = usage.output_tokens; - - if (!data.metrics.tokens && usage.totalTokens) - data.metrics.tokens = usage.totalTokens; - if (!data.metrics.prompt_tokens && usage.inputTokens) - data.metrics.prompt_tokens = usage.inputTokens; - if (!data.metrics.prompt_tokens && usage.promptTokens) - data.metrics.prompt_tokens = usage.promptTokens; - if (!data.metrics.completion_tokens && usage.outputTokens) - data.metrics.completion_tokens = usage.outputTokens; - if (!data.metrics.completion_tokens && usage.completionTokens) - data.metrics.completion_tokens = usage.completionTokens; - if (usage.input_tokens_details?.cached_tokens != null) - data.metrics.prompt_cached_tokens = - usage.input_tokens_details.cached_tokens; - if (usage.input_tokens_details?.cache_write_tokens != null) - data.metrics.prompt_cache_creation_tokens = - usage.input_tokens_details.cache_write_tokens; - } - - return data; - } - - private extractFunctionLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - if (!isFunctionSpanData(spanData)) { - return {}; - } - - return { - input: spanData.input, - output: spanData.output, - }; - } - - private extractHandoffLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - if (!isHandoffSpanData(spanData)) { - return {}; - } - return { - metadata: { - from_agent: spanData.from_agent, - to_agent: spanData.to_agent, - }, - }; - } - - private extractGuardrailLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - if (!isGuardrailSpanData(spanData)) { - return {}; - } - return { - metadata: { - triggered: spanData.triggered, - }, - }; - } - - private extractGenerationLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - if (!isGenerationSpanData(spanData)) { - return {}; - } - - const metrics: Record = {}; - - const ttft = getTimeElapsed( - span.endedAt ?? undefined, - span.startedAt ?? undefined, - ); - if (ttft !== undefined) { - metrics.time_to_first_token = ttft; - } - - const usage = spanData.usage || {}; - if (usage.prompt_tokens) metrics.prompt_tokens = usage.prompt_tokens; - else if (usage.input_tokens) metrics.prompt_tokens = usage.input_tokens; - - if (usage.completion_tokens) - metrics.completion_tokens = usage.completion_tokens; - else if (usage.output_tokens) - metrics.completion_tokens = usage.output_tokens; - - if (usage.total_tokens) metrics.tokens = usage.total_tokens; - else if (usage.input_tokens && usage.output_tokens) { - metrics.tokens = usage.input_tokens + usage.output_tokens; - } - - if (usage.input_tokens_details?.cached_tokens != null) - metrics.prompt_cached_tokens = usage.input_tokens_details.cached_tokens; - - return { - input: spanData.input, - output: spanData.output, - metadata: { - model: spanData.model, - model_config: spanData.model_config, - }, - metrics, - }; - } - - private extractCustomLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - if (!isCustomSpanData(spanData)) { - return {}; - } - return spanData.data || {}; - } - - private extractMCPListToolsLogData( - span: AgentsSpan, - ): Record { - const spanData = span.spanData; - if (!isMCPListToolsSpanData(spanData)) { - return {}; - } - return { - output: spanData.result, - metadata: { - server: spanData.server, - }, - }; - } - - private extractTranscriptionLogData( - span: AgentsSpan, - ): Record { - const spanData = span.spanData; - if (!isTranscriptionSpanData(spanData)) { - return {}; - } - return { - input: spanData.input, - output: spanData.output, - metadata: { - model: spanData.model, - model_config: spanData.model_config, - }, - }; - } - - private extractSpeechLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - if (!isSpeechSpanData(spanData)) { - return {}; - } - return { - input: spanData.input, - output: spanData.output, - metadata: { - model: spanData.model, - model_config: spanData.model_config, - }, - }; - } - - private extractSpeechGroupLogData(span: AgentsSpan): Record { - const spanData = span.spanData; - if (!isSpeechGroupSpanData(spanData)) { - return {}; - } - return { - input: spanData.input, - }; - } - - private extractLogData(span: AgentsSpan): Record { - const spanType = span.spanData?.type; - - switch (spanType) { - case SpanType.AGENT: - return this.extractAgentLogData(span); - case SpanType.RESPONSE: - return this.extractResponseLogData(span); - case SpanType.FUNCTION: - return this.extractFunctionLogData(span); - case SpanType.HANDOFF: - return this.extractHandoffLogData(span); - case SpanType.GUARDRAIL: - return this.extractGuardrailLogData(span); - case SpanType.GENERATION: - return this.extractGenerationLogData(span); - case SpanType.CUSTOM: - return this.extractCustomLogData(span); - case SpanType.MCP_TOOLS: - return this.extractMCPListToolsLogData(span); - case SpanType.TRANSCRIPTION: - return this.extractTranscriptionLogData(span); - case SpanType.SPEECH: - return this.extractSpeechLogData(span); - case SpanType.SPEECH_GROUP: - return this.extractSpeechGroupLogData(span); - default: - return {}; - } - } - - onSpanStart(span: AgentsSpan): Promise { - if (!span.spanId || !span.traceId) return Promise.resolve(); - - const traceData = this.traceSpans.get(span.traceId); - if (!traceData) return Promise.resolve(); - - let parentSpan: BraintrustSpan | undefined; - if (span.parentId) { - parentSpan = traceData.childSpans.get(span.parentId); - } else { - parentSpan = traceData.rootSpan; - } - - if (parentSpan) { - const childSpan = parentSpan.startSpan({ - name: spanNameFromAgents(span), - type: spanTypeFromAgents(span), - }); - traceData.childSpans.set(span.spanId, childSpan); - } - return Promise.resolve(); - } - - onSpanEnd(span: AgentsSpan): Promise { - if (!span.spanId || !span.traceId) return Promise.resolve(); - - const traceData = this.traceSpans.get(span.traceId); - if (!traceData) return Promise.resolve(); - - const braintrustSpan = traceData.childSpans.get(span.spanId); - - if (braintrustSpan) { - const logData = this.extractLogData(span); - braintrustSpan.log({ - error: span.error, - ...logData, - }); - braintrustSpan.end(); - traceData.childSpans.delete(span.spanId); - - const input = logData.input as SpanInput; - const output = logData.output as SpanOutput; - - if (traceData.metadata.firstInput === null && input != null) { - traceData.metadata.firstInput = input; - } - - if (output != null) { - traceData.metadata.lastOutput = output; - } - } else { - console.warn(`No span found for ID: ${span.spanId}`); - } - return Promise.resolve(); - } - - async shutdown(): Promise { - if (this.logger && typeof this.logger.flush === "function") { - await this.logger.flush(); - } - } - - async forceFlush(): Promise { - if (this.logger && typeof this.logger.flush === "function") { - await this.logger.flush(); - } - } -} diff --git a/integrations/openai-agents-js/src/openai-agents-integration.test.ts b/integrations/openai-agents-js/src/openai-agents-integration.test.ts deleted file mode 100644 index e6f59c0cd..000000000 --- a/integrations/openai-agents-js/src/openai-agents-integration.test.ts +++ /dev/null @@ -1,1264 +0,0 @@ -import { - test, - assert, - beforeEach, - beforeAll, - afterEach, - describe, -} from "vitest"; -import { OpenAIAgentsTraceProcessor } from "./index"; -import { z } from "zod/v3"; - -// Import necessary types and functions from braintrust -import { - initLogger, - Logger, - Span as BraintrustSpan, - wrapTraced, - currentSpan, -} from "braintrust"; -import { - _exportsForTestingOnly, - type TestBackgroundLogger, -} from "@braintrust-test/logger"; - -// Test helper functions for backward compatibility -function getSpansMap( - processor: OpenAIAgentsTraceProcessor, -): Map { - const spans = new Map(); - for (const [traceId, traceData] of processor._traceSpans) { - spans.set(traceId, traceData.rootSpan); - for (const [spanId, span] of traceData.childSpans) { - spans.set(`${traceId}:${spanId}`, span); - } - } - return spans; -} - -function getTraceMetadataMap(processor: OpenAIAgentsTraceProcessor) { - const metadata = new Map(); - for (const [traceId, traceData] of processor._traceSpans) { - metadata.set(traceId, traceData.metadata); - } - return metadata; -} - -const TEST_SUITE_OPTIONS = { timeout: 30000, retry: 3 }; - -// Simple test model for OpenAI Agents -const TEST_MODEL = "gpt-4o-mini"; - -// Node.js configuration is automatically handled when importing from braintrust - -// Test with real @openai/agents SDK calls (requires OPENAI_API_KEY) -describe( - "OpenAI Agents tracing processor integration tests", - TEST_SUITE_OPTIONS, - () => { - let backgroundLogger: TestBackgroundLogger; - let _logger: Logger; - let Agent: any; - let run: any; - let tool: any; - let setTraceProcessors: any; - let addTraceProcessor: any; - let setTracingDisabled: any; - - beforeAll(async () => { - await _exportsForTestingOnly.simulateLoginForTests(); - - // Dynamically import @openai/agents to handle cases where it's not available - try { - const agentsModule = await import("@openai/agents"); - Agent = agentsModule.Agent; - run = agentsModule.run; - tool = agentsModule.tool; - setTraceProcessors = agentsModule.setTraceProcessors; - addTraceProcessor = agentsModule.addTraceProcessor; - setTracingDisabled = agentsModule.setTracingDisabled; - } catch (error) { - console.warn( - "@openai/agents not available, skipping integration tests", - ); - } - }); - - beforeEach(() => { - backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); - _logger = initLogger({ - projectName: "openai-agents.test.ts", - projectId: "test-openai-agents", - }); - }); - - afterEach(() => { - if (_logger) { - _logger.flush(); - } - if (setTraceProcessors) { - setTraceProcessors([]); - } - _exportsForTestingOnly.clearTestBackgroundLogger(); - }); - - test("OpenAIAgentsTraceProcessor is instantiable", () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - assert.ok(processor); - - // Test methods exist - assert.ok(typeof processor.onTraceStart === "function"); - assert.ok(typeof processor.onTraceEnd === "function"); - assert.ok(typeof processor.onSpanStart === "function"); - assert.ok(typeof processor.onSpanEnd === "function"); - assert.ok(typeof processor.shutdown === "function"); - assert.ok(typeof processor.forceFlush === "function"); - }); - - test("simple agent run with tracing", async (context) => { - assert.lengthOf(await backgroundLogger.drain(), 0); - - // Set up the OpenAI Agents tracing processor - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - setTracingDisabled(false); - addTraceProcessor(processor); - - try { - // Create a simple agent - const agent = new Agent({ - name: "test-agent", - model: TEST_MODEL, - instructions: "You are a helpful assistant. Be concise.", - }); - - // Run the agent with a simple prompt using the run() function - const result = await run(agent, "What is 2+2? Just give the number."); - assert.ok(result); - assert.ok(result.finalOutput); - - // Verify spans were created - const spans = await backgroundLogger.drain(); - assert.isTrue( - spans.length > 0, - "Expected at least one span to be created", - ); - - // Verify span structure - const traceSpan = spans.find( - (s: any) => s.span_attributes?.type === "task", - ); - assert.ok( - traceSpan, - "Expected to find a task-type span for the agent trace", - ); - assert.equal((traceSpan as any).span_attributes.name, "Agent workflow"); - } finally { - processor.shutdown(); - } - }); - - test("agent with function calling", async (context) => { - assert.lengthOf(await backgroundLogger.drain(), 0); - - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - setTracingDisabled(false); - addTraceProcessor(processor); - - try { - // Create a tool using the proper tool() helper - const getWeatherTool = tool({ - name: "get_weather", - description: "Get the current weather for a city", - parameters: z.object({ city: z.string() }), - execute: async (input: { city: string }) => { - return `The weather in ${input.city} is sunny with temperature 72°F`; - }, - }); - - // Create agent with the tool - const agent = new Agent({ - name: "weather-agent", - model: TEST_MODEL, - instructions: - "You can get the weather for any city. Use the get_weather tool when asked about weather.", - tools: [getWeatherTool], - }); - - const result = await run(agent, "What's the weather in San Francisco?"); - assert.ok(result); - assert.ok(result.finalOutput); - - // Verify spans were created - const spans = await backgroundLogger.drain(); - assert.isTrue(spans.length > 0); - - // Verify span structure - const taskSpans = spans.filter( - (s: any) => s.span_attributes?.type === "task", - ); - assert.isTrue(taskSpans.length > 0, "Expected task-type spans"); - - // Verify tool spans if function calling occurred - const toolSpans = spans.filter( - (s: any) => s.span_attributes?.type === "tool", - ); - if (toolSpans.length > 0) { - const toolSpan = toolSpans[0] as any; - assert.ok( - toolSpan.span_attributes.name, - "Tool span should have a name", - ); - } - } finally { - processor.shutdown(); - } - }); - - test("Cleanup behavior - traces are cleaned up properly and orphaned spans are handled gracefully", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-cleanup", - name: "cleanup-test", - metadata: {}, - }; - - // Start trace and verify it's stored - await processor.onTraceStart(trace); - assert.isTrue( - getSpansMap(processor).has(trace.traceId), - "Root span should be stored", - ); - assert.isTrue( - getTraceMetadataMap(processor).has(trace.traceId), - "Trace metadata should be stored", - ); - - // Add a child span - const span = { - spanId: "test-span-cleanup", - traceId: trace.traceId, - spanData: { type: "agent", name: "test-agent" }, - error: null, - } as any; - - await processor.onSpanStart(span); - const childSpanKey = `${trace.traceId}:${span.spanId}`; - assert.isTrue( - getSpansMap(processor).has(childSpanKey), - "Child span should be stored", - ); - - // End the child span first - await processor.onSpanEnd(span); - - // End the trace normally - await processor.onTraceEnd(trace); - - // Verify cleanup happened - assert.isFalse( - getSpansMap(processor).has(trace.traceId), - "Root span should be removed", - ); - assert.isFalse( - getTraceMetadataMap(processor).has(trace.traceId), - "Trace metadata should be removed", - ); - - // Test that operations with orphaned spans are handled gracefully - const orphanedSpan = { - spanId: "orphaned-span", - traceId: "test-trace-cleanup", // Same traceId but trace is now gone - spanData: { type: "agent", name: "orphaned" }, - error: null, - } as any; - - // These should be no-ops and not throw - await processor.onSpanStart(orphanedSpan); - await processor.onSpanEnd(orphanedSpan); - - // Verify the orphaned operations didn't create any trace data - assert.isFalse( - getSpansMap(processor).has("test-trace-cleanup"), - "Orphaned operations shouldn't recreate root span", - ); - assert.isFalse( - getTraceMetadataMap(processor).has("test-trace-cleanup"), - "Orphaned operations shouldn't recreate metadata", - ); - assert.equal( - getSpansMap(processor).size, - 0, - "No spans should exist after cleanup", - ); - assert.equal( - getTraceMetadataMap(processor).size, - 0, - "No metadata should exist after cleanup", - ); - }); - - test("LRU eviction behavior - oldest traces are evicted when maxTraces is exceeded", async () => { - // Use a small maxTraces for fast testing - const maxTraces = 3; - const processor = new OpenAIAgentsTraceProcessor({ maxTraces }); - - // Create traces up to the limit - const traces: any[] = []; - for (let i = 0; i < maxTraces; i++) { - const trace = { - traceId: `test-trace-${i}`, - name: `test-${i}`, - metadata: {}, - } as any; - traces.push(trace); - await processor.onTraceStart(trace); - } - - // Verify all traces are stored - assert.equal( - getTraceMetadataMap(processor).size, - maxTraces, - "All trace metadata should be stored", - ); - assert.isTrue( - getSpansMap(processor).has("test-trace-0"), - "First trace root span should exist", - ); - assert.isTrue( - getSpansMap(processor).has("test-trace-1"), - "Second trace root span should exist", - ); - assert.isTrue( - getSpansMap(processor).has("test-trace-2"), - "Third trace root span should exist", - ); - - // Add one more trace - this should trigger LRU eviction - const newTrace = { - traceId: "test-trace-new", - name: "test-new", - metadata: {}, - } as any; - await processor.onTraceStart(newTrace); - - // Metadata should still be at max size - assert.equal( - getTraceMetadataMap(processor).size, - maxTraces, - "Metadata should remain at max size after eviction", - ); - - // First trace should be evicted, new trace should exist - assert.isFalse( - getSpansMap(processor).has("test-trace-0"), - "First (oldest) trace should be evicted", - ); - assert.isFalse( - getTraceMetadataMap(processor).has("test-trace-0"), - "First trace metadata should be evicted", - ); - assert.isTrue( - getSpansMap(processor).has("test-trace-new"), - "New trace should exist", - ); - assert.isTrue( - getTraceMetadataMap(processor).has("test-trace-new"), - "New trace metadata should exist", - ); - assert.isTrue( - getSpansMap(processor).has("test-trace-1"), - "Second trace should still exist", - ); - assert.isTrue( - getSpansMap(processor).has("test-trace-2"), - "Third trace should still exist", - ); - }); - - test("Span hierarchy and storage validation - ensures proper parent-child relationships", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - // Create a trace - const trace: any = { - traceId: "test-hierarchy-trace", - name: "hierarchy-test", - metadata: {}, - }; - - await processor.onTraceStart(trace); - assert.isTrue( - getSpansMap(processor).has(trace.traceId), - "Root span should be stored by traceId", - ); - assert.isTrue( - getTraceMetadataMap(processor).has(trace.traceId), - "Trace metadata should be stored", - ); - - // Create parent span (no parentId, should attach to root) - const parentSpan = { - spanId: "parent-span-1", - traceId: trace.traceId, - parentId: null, - spanData: { type: "agent", name: "parent-agent" }, - error: null, - } as any; - - await processor.onSpanStart(parentSpan); - const parentSpanKey = `${trace.traceId}:${parentSpan.spanId}`; - assert.isTrue( - getSpansMap(processor).has(parentSpanKey), - "Parent span should be stored with composite key", - ); - - // Create child span (with parentId, should attach to parent) - const childSpan = { - spanId: "child-span-1", - traceId: trace.traceId, - parentId: parentSpan.spanId, - spanData: { - type: "function", - name: "child-function", - input: "test input", - output: "test output", - }, - error: null, - } as any; - - await processor.onSpanStart(childSpan); - const childSpanKey = `${trace.traceId}:${childSpan.spanId}`; - assert.isTrue( - getSpansMap(processor).has(childSpanKey), - "Child span should be stored with composite key", - ); - - // Create grandchild span - const grandchildSpan = { - spanId: "grandchild-span-1", - traceId: trace.traceId, - parentId: childSpan.spanId, - spanData: { - type: "generation", - name: "grandchild-generation", - input: [{ role: "user", content: "test" }], - output: [{ role: "assistant", content: "response" }], - }, - error: null, - } as any; - - await processor.onSpanStart(grandchildSpan); - const grandchildSpanKey = `${trace.traceId}:${grandchildSpan.spanId}`; - assert.isTrue( - getSpansMap(processor).has(grandchildSpanKey), - "Grandchild span should be stored with composite key", - ); - - // Verify we have the expected number of spans - const allSpanKeys = Array.from(getSpansMap(processor).keys()); - const traceSpans = allSpanKeys.filter((key) => - key.startsWith(trace.traceId), - ); - assert.equal( - traceSpans.length, - 4, - "Should have 4 spans total: 1 root + 3 child spans", - ); - - // End spans in reverse order (grandchild -> child -> parent) - await processor.onSpanEnd(grandchildSpan); - assert.isFalse( - getSpansMap(processor).has(grandchildSpanKey), - "Grandchild span should be removed after ending", - ); - - await processor.onSpanEnd(childSpan); - assert.isFalse( - getSpansMap(processor).has(childSpanKey), - "Child span should be removed after ending", - ); - - await processor.onSpanEnd(parentSpan); - assert.isFalse( - getSpansMap(processor).has(parentSpanKey), - "Parent span should be removed after ending", - ); - - // Root span should still exist - assert.isTrue( - getSpansMap(processor).has(trace.traceId), - "Root span should still exist", - ); - assert.isTrue( - getTraceMetadataMap(processor).has(trace.traceId), - "Trace metadata should still exist", - ); - - // End the trace - await processor.onTraceEnd(trace); - assert.isFalse( - getSpansMap(processor).has(trace.traceId), - "Root span should be removed after trace end", - ); - assert.isFalse( - getTraceMetadataMap(processor).has(trace.traceId), - "Trace metadata should be removed after trace end", - ); - - // Verify all spans are cleaned up - const remainingSpans = Array.from(getSpansMap(processor).keys()).filter( - (key) => key.startsWith(trace.traceId), - ); - assert.equal( - remainingSpans.length, - 0, - "No spans should remain for this trace", - ); - }); - - test("currentSpan() detection creates proper span hierarchy with actual OpenAI SDK", async () => { - // This tests that currentSpan() detection works with the real OpenAI SDK - assert.lengthOf(await backgroundLogger.drain(), 0); - - const testFunction = wrapTraced( - async (instructions: string) => { - // Verify we're in a traced context - const detectedParent = currentSpan(); - assert.ok( - detectedParent, - "Parent span should exist in traced context", - ); - - // Create processor WITHOUT parentSpan - should auto-detect via currentSpan() - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - setTracingDisabled(false); - addTraceProcessor(processor); - - try { - // Create a simple agent - const agent = new Agent({ - name: "test-agent", - model: TEST_MODEL, - instructions: "You are a helpful assistant. Be very concise.", - }); - - // Run the agent - this should create spans as children of detected parent - const result = await run(agent, instructions); - assert.ok(result, "Agent should return a result"); - assert.ok(result.finalOutput, "Result should have finalOutput"); - - return result; - } finally { - processor.shutdown(); - } - }, - { name: "parent_span_test" }, - ); - - // Execute the wrapped function - const result = await testFunction("What is 2+2? Just the number."); - assert.ok(result, "Test function should return a result"); - - // Verify span hierarchy in logged spans - const spans = await backgroundLogger.drain(); - assert.isTrue( - spans.length >= 2, - "Should have at least parent and child spans", - ); - - // Find parent and child spans - const parentSpan = spans.find( - (s: any) => s.span_attributes?.name === "parent_span_test", - ); - const childSpan = spans.find( - (s: any) => s.span_attributes?.name === "Agent workflow", - ); - - assert.ok( - parentSpan, - "Should find parent span with name 'parent_span_test'", - ); - assert.ok(childSpan, "Should find child span with name 'Agent workflow'"); - - // Verify the child span has the parent as its parent - if (childSpan && parentSpan) { - // In Braintrust, parent-child relationships are represented by span_parents array - const childSpanParents = (childSpan as any).span_parents || []; - const parentSpanId = (parentSpan as any).span_id; - - assert.ok( - Array.isArray(childSpanParents) && childSpanParents.length > 0, - "Child span should have span_parents array", - ); - assert.isTrue( - childSpanParents.includes(parentSpanId), - "Child span should include parent span_id in its span_parents array (currentSpan detection)", - ); - - // Verify both spans have the same root_span_id - assert.equal( - (childSpan as any).root_span_id, - (parentSpan as any).root_span_id, - "Parent and child should share the same root_span_id", - ); - } - - // Verify input/output are properly logged on parent span - assert.ok( - (parentSpan as any).input, - "Parent span should have input logged", - ); - assert.ok( - (parentSpan as any).output, - "Parent span should have output logged", - ); - - // Verify that we have child spans beyond just "Agent workflow" - // The OpenAI SDK should generate multiple span types (generation, response, etc.) - const allChildSpans = spans.filter((s: any) => - (s as any).span_parents?.includes((parentSpan as any).span_id), - ); - - assert.isTrue( - allChildSpans.length >= 1, - `Should have at least 1 child span, but found ${allChildSpans.length}`, - ); - - // We should see spans like Generation, Response, etc. from the OpenAI SDK - const spanTypes = allChildSpans.map((s: any) => s.span_attributes?.type); - const hasLLMSpans = spanTypes.includes("llm"); - const hasTaskSpans = spanTypes.includes("task"); - - assert.isTrue( - hasLLMSpans || hasTaskSpans, - "Should have LLM or task type spans from OpenAI SDK", - ); - }); - - test("processor without parentSpan creates root spans (backward compatibility)", async () => { - // This ensures backward compatibility when parentSpan is not provided - assert.lengthOf(await backgroundLogger.drain(), 0); - - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - // No parentSpan provided - should create root spans - }); - - setTracingDisabled(false); - addTraceProcessor(processor); - - try { - const agent = new Agent({ - name: "root-agent", - model: TEST_MODEL, - instructions: "Be concise.", - }); - - const result = await run(agent, "What is 2+2?"); - assert.ok(result); - - const spans = await backgroundLogger.drain(); - assert.isTrue(spans.length > 0, "Should create spans"); - - // Find the Agent workflow span - const agentSpan = spans.find( - (s: any) => s.span_attributes?.name === "Agent workflow", - ); - assert.ok(agentSpan, "Should find Agent workflow span"); - - // Verify it's a root span (no parent_id or parent_id is null) - const isRootSpan = - !(agentSpan as any).parent_id || - (agentSpan as any).parent_id === null; - assert.isTrue( - isRootSpan, - "Agent workflow should be a root span when no parentSpan provided", - ); - } finally { - processor.shutdown(); - } - }); - - test("onTraceEnd logs trace metadata properly", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-metadata", - name: "metadata-test", - metadata: { userId: "test-user-123", sessionId: "session-456" }, - }; - - const span = { - spanId: "test-span", - traceId: trace.traceId, - spanData: { - type: "response", - name: "test-response", - _input: "test input", - _response: { output: "test output" }, - }, - error: null, - } as any; - - // Execute trace lifecycle - await processor.onTraceStart(trace); - await processor.onSpanStart(span); - await processor.onSpanEnd(span); - await processor.onTraceEnd(trace); - - // Verify metadata was logged to root span - const spans = await backgroundLogger.drain(); - const rootSpan = spans.find( - (s: any) => s.span_attributes?.name === "metadata-test", - ); - - assert.ok(rootSpan, "Should find root span"); - const spanMetadata = (rootSpan as any).metadata; - assert.ok(spanMetadata, "Root span should have metadata"); - assert.equal( - spanMetadata.userId, - "test-user-123", - "Should log trace metadata", - ); - assert.equal( - spanMetadata.sessionId, - "session-456", - "Should log trace metadata", - ); - }); - - test("response span with image generation converts base64 to attachments", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-images", - name: "image-test", - metadata: {}, - }; - - // Create a small base64-encoded PNG (1x1 red pixel) - const smallBase64Image = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; - - // Simulate response span with image input and output - const span = { - spanId: "test-span-images", - traceId: trace.traceId, - spanData: { - type: "response", - name: "test-response-images", - _input: [ - { - role: "user", - content: [ - { - type: "input_text", - text: "Change the background to Japanese style", - }, - { - type: "input_image", - image: `data:image/png;base64,${smallBase64Image}`, - }, - ], - }, - ], - _response: { - output: [ - { - id: "ig_test123", - type: "image_generation_call", - status: "completed", - output_format: "png", - result: smallBase64Image, - revised_prompt: "A Japanese style background", - }, - { - id: "msg_test456", - type: "message", - status: "completed", - content: [ - { - type: "output_text", - text: "Here is the image with a Japanese style background", - }, - ], - role: "assistant", - }, - ], - usage: { - input_tokens: 1555, - output_tokens: 113, - total_tokens: 1668, - }, - }, - }, - error: null, - } as any; - - // Execute trace lifecycle - await processor.onTraceStart(trace); - await processor.onSpanStart(span); - await processor.onSpanEnd(span); - await processor.onTraceEnd(trace); - - // Verify spans were created - const spans = await backgroundLogger.drain(); - const responseSpan = spans.find( - (s: any) => s.span_attributes?.name === "test-response-images", - ); - - assert.ok(responseSpan, "Should find response span"); - - // Verify input image was converted to attachment - const input = (responseSpan as any).input; - assert.ok(input, "Response span should have input"); - assert.ok(Array.isArray(input), "Input should be an array"); - - const userMessage = input[0]; - assert.ok(userMessage.content, "User message should have content"); - - const imageContent = userMessage.content.find( - (c: any) => c.type === "input_image", - ); - assert.ok(imageContent, "Should find input_image in content"); - assert.ok(imageContent.image, "Image content should have image field"); - - // Check if it's an Attachment (has the Braintrust attachment structure) - const imageAttachment = imageContent.image; - assert.ok( - imageAttachment.filename || imageAttachment._data, - "Image should be converted to Attachment object", - ); - - // Verify output image was converted to attachment - const output = (responseSpan as any).output; - assert.ok(output, "Response span should have output"); - assert.ok(Array.isArray(output), "Output should be an array"); - - const imageGenCall = output.find( - (o: any) => o.type === "image_generation_call", - ); - assert.ok(imageGenCall, "Should find image_generation_call in output"); - assert.ok(imageGenCall.result, "Image generation should have result"); - - // Check if result is an Attachment - const resultAttachment = imageGenCall.result; - assert.ok( - resultAttachment.filename || resultAttachment._data, - "Result should be converted to Attachment object", - ); - - // Verify metrics were extracted - const metrics = (responseSpan as any).metrics; - assert.ok(metrics, "Response span should have metrics"); - assert.equal(metrics.prompt_tokens, 1555, "Should have prompt_tokens"); - assert.equal( - metrics.completion_tokens, - 113, - "Should have completion_tokens", - ); - assert.equal(metrics.tokens, 1668, "Should have total tokens"); - }); - - test("Response span extracts cached tokens from usage", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - // Create a mock trace - const trace: any = { - traceId: "test-trace-cached-response", - name: "cached-response-test", - metadata: {}, - }; - - await processor.onTraceStart(trace); - - // Create a mock Response span with cached tokens - const responseSpan: any = { - spanId: "test-response-span", - traceId: trace.traceId, - parentId: null, - startedAt: new Date().toISOString(), - endedAt: new Date(Date.now() + 1000).toISOString(), - spanData: { - type: "response", - _input: "test input", - _response: { - output: "test output", - usage: { - input_tokens: 100, - output_tokens: 50, - total_tokens: 150, - input_tokens_details: { - cached_tokens: 80, // check for this later - }, - }, - }, - }, - error: null, - }; - - // Process the span - await processor.onSpanStart(responseSpan); - await processor.onSpanEnd(responseSpan); - - // Get the logged spans - const spans = await backgroundLogger.drain(); - - // Find the response span - const responseSpanLog = spans.find( - (s: any) => s.span_attributes?.type === "llm", - ); - - assert.ok(responseSpanLog, "Response span should be logged"); - - // Verify metrics were extracted - const metrics = (responseSpanLog as any).metrics; - assert.ok(metrics, "Response span should have metrics"); - assert.equal(metrics.prompt_tokens, 100, "Should have prompt_tokens"); - assert.equal( - metrics.completion_tokens, - 50, - "Should have completion_tokens", - ); - assert.equal(metrics.tokens, 150, "Should have total tokens"); - assert.equal( - metrics.prompt_cached_tokens, - 80, - "Should extract cached_tokens to prompt_cached_tokens", - ); - }); - - test("Response span handles zero cached tokens correctly", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-zero-cached", - name: "zero-cached-test", - metadata: {}, - }; - - await processor.onTraceStart(trace); - - const responseSpan: any = { - spanId: "test-response-span-zero", - traceId: trace.traceId, - parentId: null, - startedAt: new Date().toISOString(), - endedAt: new Date(Date.now() + 1000).toISOString(), - spanData: { - type: "response", - _response: { - usage: { - input_tokens: 100, - output_tokens: 50, - input_tokens_details: { - cached_tokens: 0, // Zero is a valid value - }, - }, - }, - }, - error: null, - }; - - await processor.onSpanStart(responseSpan); - await processor.onSpanEnd(responseSpan); - - const spans = await backgroundLogger.drain(); - const responseSpanLog = spans.find( - (s: any) => s.span_attributes?.type === "llm", - ); - const metrics = (responseSpanLog as any).metrics; - - // Zero should be logged, not skipped - assert.equal( - metrics.prompt_cached_tokens, - 0, - "Should log cached_tokens even when zero", - ); - }); - - test("Response span extracts cache write tokens from usage", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-cache-write-response", - name: "cache-write-response-test", - metadata: {}, - }; - - await processor.onTraceStart(trace); - - const responseSpan: any = { - spanId: "test-response-span-cache-write", - traceId: trace.traceId, - parentId: null, - startedAt: new Date().toISOString(), - endedAt: new Date(Date.now() + 1000).toISOString(), - spanData: { - type: "response", - _response: { - usage: { - input_tokens: 100, - output_tokens: 50, - total_tokens: 150, - input_tokens_details: { - cache_write_tokens: 25, - }, - }, - }, - }, - error: null, - }; - - await processor.onSpanStart(responseSpan); - await processor.onSpanEnd(responseSpan); - - const spans = await backgroundLogger.drain(); - const responseSpanLog = spans.find( - (s: any) => s.span_attributes?.type === "llm", - ); - const metrics = (responseSpanLog as any).metrics; - - assert.equal( - metrics.prompt_cache_creation_tokens, - 25, - "Should extract cache_write_tokens to prompt_cache_creation_tokens", - ); - }); - - test("Response span handles zero cache write tokens correctly", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-zero-cache-write", - name: "zero-cache-write-test", - metadata: {}, - }; - - await processor.onTraceStart(trace); - - const responseSpan: any = { - spanId: "test-response-span-zero-cache-write", - traceId: trace.traceId, - parentId: null, - startedAt: new Date().toISOString(), - endedAt: new Date(Date.now() + 1000).toISOString(), - spanData: { - type: "response", - _response: { - usage: { - input_tokens: 100, - output_tokens: 50, - input_tokens_details: { - cache_write_tokens: 0, - }, - }, - }, - }, - error: null, - }; - - await processor.onSpanStart(responseSpan); - await processor.onSpanEnd(responseSpan); - - const spans = await backgroundLogger.drain(); - const responseSpanLog = spans.find( - (s: any) => s.span_attributes?.type === "llm", - ); - const metrics = (responseSpanLog as any).metrics; - - // Zero should be logged, not skipped - assert.equal( - metrics.prompt_cache_creation_tokens, - 0, - "Should log cache_write_tokens even when zero", - ); - }); - - test("Response span handles missing cache write tokens gracefully", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-no-cache-write", - name: "no-cache-write-test", - metadata: {}, - }; - - await processor.onTraceStart(trace); - - const responseSpan: any = { - spanId: "test-response-span-no-cache-write", - traceId: trace.traceId, - parentId: null, - startedAt: new Date().toISOString(), - endedAt: new Date(Date.now() + 1000).toISOString(), - spanData: { - type: "response", - _response: { - usage: { - input_tokens: 100, - output_tokens: 50, - input_tokens_details: { - cached_tokens: 40, - // No cache_write_tokens - }, - }, - }, - }, - error: null, - }; - - await processor.onSpanStart(responseSpan); - await processor.onSpanEnd(responseSpan); - - const spans = await backgroundLogger.drain(); - const responseSpanLog = spans.find( - (s: any) => s.span_attributes?.type === "llm", - ); - const metrics = (responseSpanLog as any).metrics; - - assert.isUndefined( - metrics.prompt_cache_creation_tokens, - "Should not add prompt_cache_creation_tokens if not in usage", - ); - }); - - test("Response span handles missing cached tokens gracefully", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-no-cached", - name: "no-cached-test", - metadata: {}, - }; - - await processor.onTraceStart(trace); - - const responseSpan: any = { - spanId: "test-response-span-no-cached", - traceId: trace.traceId, - parentId: null, - startedAt: new Date().toISOString(), - endedAt: new Date(Date.now() + 1000).toISOString(), - spanData: { - type: "response", - _response: { - usage: { - input_tokens: 100, - output_tokens: 50, - // No input_tokens_details at all - }, - }, - }, - error: null, - }; - - await processor.onSpanStart(responseSpan); - await processor.onSpanEnd(responseSpan); - - const spans = await backgroundLogger.drain(); - const responseSpanLog = spans.find( - (s: any) => s.span_attributes?.type === "llm", - ); - const metrics = (responseSpanLog as any).metrics; - - // Should not have prompt_cached_tokens if not present in usage - assert.isUndefined( - metrics.prompt_cached_tokens, - "Should not add prompt_cached_tokens if not in usage", - ); - }); - - test("Generation span extracts cached tokens from usage", async () => { - const processor = new OpenAIAgentsTraceProcessor({ - logger: _logger as any, - }); - - const trace: any = { - traceId: "test-trace-cached-generation", - name: "cached-generation-test", - metadata: {}, - }; - - await processor.onTraceStart(trace); - - const generationSpan: any = { - spanId: "test-generation-span", - traceId: trace.traceId, - parentId: null, - startedAt: new Date().toISOString(), - endedAt: new Date(Date.now() + 1000).toISOString(), - spanData: { - type: "generation", - input: [{ role: "user", content: "test" }], - output: [{ role: "assistant", content: "response" }], - model: "gpt-4o-mini", - usage: { - input_tokens: 200, - output_tokens: 75, - total_tokens: 275, - input_tokens_details: { - cached_tokens: 150, // Test Generation span extraction - }, - }, - }, - error: null, - }; - - await processor.onSpanStart(generationSpan); - await processor.onSpanEnd(generationSpan); - - const spans = await backgroundLogger.drain(); - const generationSpanLog = spans.find( - (s: any) => s.span_attributes?.type === "llm", - ); - - assert.ok(generationSpanLog, "Generation span should be logged"); - - const metrics = (generationSpanLog as any).metrics; - assert.ok(metrics, "Generation span should have metrics"); - assert.equal(metrics.prompt_tokens, 200, "Should have prompt_tokens"); - assert.equal( - metrics.completion_tokens, - 75, - "Should have completion_tokens", - ); - assert.equal( - metrics.prompt_cached_tokens, - 150, - "Should extract cached_tokens from Generation span", - ); - }); - }, -); diff --git a/integrations/openai-agents-js/src/openai-agents-trace-processor.test.ts b/integrations/openai-agents-js/src/openai-agents-trace-processor.test.ts deleted file mode 100644 index 3b7fcbdda..000000000 --- a/integrations/openai-agents-js/src/openai-agents-trace-processor.test.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { assert, describe, test } from "vitest"; -import { OpenAIAgentsTraceProcessor } from "./index"; - -function createDeferredPromise(): { - promise: Promise; - resolve: (value: T | PromiseLike) => void; - reject: (reason?: unknown) => void; -} { - let resolve!: (value: T | PromiseLike) => void; - let reject!: (reason?: unknown) => void; - const promise = new Promise((resolvePromise, rejectPromise) => { - resolve = resolvePromise; - reject = rejectPromise; - }); - return { promise, resolve, reject }; -} - -function trackAsyncCompletion(promise: Promise): { - promise: Promise; - isResolved: () => boolean; -} { - let resolved = false; - return { - promise: promise.finally(() => { - resolved = true; - }), - isResolved: () => resolved, - }; -} - -describe("OpenAIAgentsTraceProcessor flush behavior", () => { - test("onTraceEnd waits for root span flush to complete", async () => { - let flushCalls = 0; - let endCalls = 0; - const deferred = createDeferredPromise(); - let rootSpanInput: unknown; - let rootSpanOutput: unknown; - - const childSpan = { - log: () => {}, - end: () => {}, - }; - - const rootSpan = { - log: (data: Record) => { - rootSpanInput = data.input; - rootSpanOutput = data.output; - }, - startSpan: () => childSpan, - end: () => { - endCalls += 1; - }, - flush: () => { - flushCalls += 1; - return deferred.promise; - }, - }; - - const processor = new OpenAIAgentsTraceProcessor({ - logger: { - startSpan: () => rootSpan, - } as any, - }); - - const trace = { - traceId: "trace-1", - name: "test-trace", - groupId: "group-1", - metadata: {}, - } as any; - - await processor.onTraceStart(trace); - - const childOpenAIAgentsSpan = { - spanId: "span-1", - traceId: trace.traceId, - spanData: { - type: "generation", - input: "first-input", - output: "last-output", - }, - error: null, - } as any; - await processor.onSpanStart(childOpenAIAgentsSpan); - await processor.onSpanEnd(childOpenAIAgentsSpan); - - const onTraceEndCompletion = trackAsyncCompletion( - processor.onTraceEnd(trace), - ); - - await Promise.resolve(); - - assert.equal(endCalls, 1, "onTraceEnd should end the root span"); - assert.equal(flushCalls, 1, "onTraceEnd should flush the root span once"); - assert.equal( - rootSpanInput, - "first-input", - "onTraceEnd should log first input", - ); - assert.equal( - rootSpanOutput, - "last-output", - "onTraceEnd should log last output", - ); - assert.isFalse( - onTraceEndCompletion.isResolved(), - "onTraceEnd should not resolve before root span flush resolves", - ); - - deferred.resolve(); - await onTraceEndCompletion.promise; - - assert.isTrue( - onTraceEndCompletion.isResolved(), - "onTraceEnd should resolve after root span flush resolves", - ); - assert.isFalse( - processor._traceSpans.has(trace.traceId), - "onTraceEnd should remove trace state after finishing", - ); - }); - - test("onTraceEnd propagates root span flush failure after cleanup", async () => { - let flushCalls = 0; - let endCalls = 0; - const deferred = createDeferredPromise(); - const failure = new Error("flush failed"); - let rootSpanInput: unknown; - let rootSpanOutput: unknown; - - const childSpan = { - log: () => {}, - end: () => {}, - }; - - const rootSpan = { - log: (data: Record) => { - rootSpanInput = data.input; - rootSpanOutput = data.output; - }, - startSpan: () => childSpan, - end: () => { - endCalls += 1; - }, - flush: () => { - flushCalls += 1; - return deferred.promise; - }, - }; - - const processor = new OpenAIAgentsTraceProcessor({ - logger: { - startSpan: () => rootSpan, - } as any, - }); - - const trace = { - traceId: "trace-2", - name: "test-trace-fail", - groupId: "group-1", - metadata: {}, - } as any; - - await processor.onTraceStart(trace); - - const childOpenAIAgentsSpan = { - spanId: "span-1", - traceId: trace.traceId, - spanData: { - type: "generation", - input: "first-input", - output: "last-output", - }, - error: null, - } as any; - await processor.onSpanStart(childOpenAIAgentsSpan); - await processor.onSpanEnd(childOpenAIAgentsSpan); - - const onTraceEndCompletion = trackAsyncCompletion( - processor.onTraceEnd(trace), - ); - - await Promise.resolve(); - - assert.equal(endCalls, 1, "onTraceEnd should end the root span"); - assert.equal(flushCalls, 1, "onTraceEnd should flush the root span once"); - assert.equal( - rootSpanInput, - "first-input", - "root span log should include first input", - ); - assert.equal( - rootSpanOutput, - "last-output", - "root span log should include last output", - ); - assert.isFalse( - onTraceEndCompletion.isResolved(), - "onTraceEnd should wait for root span flush promise", - ); - - deferred.reject(failure); - try { - await onTraceEndCompletion.promise; - assert.fail("onTraceEnd should reject when root span flush rejects"); - } catch (error) { - assert.instanceOf(error as Error, Error); - assert.equal((error as Error).message, "flush failed"); - } - - assert.isFalse( - processor._traceSpans.has(trace.traceId), - "onTraceEnd should remove trace state even when flush fails", - ); - }); - - test("forceFlush waits for logger.flush to complete", async () => { - let flushCalls = 0; - const deferred = createDeferredPromise(); - - const processor = new OpenAIAgentsTraceProcessor({ - logger: { - flush: () => { - flushCalls += 1; - return deferred.promise; - }, - } as any, - }); - - const forceFlushCompletion = trackAsyncCompletion(processor.forceFlush()); - - await Promise.resolve(); - - assert.equal( - flushCalls, - 1, - "forceFlush should call logger.flush exactly once", - ); - assert.isFalse( - forceFlushCompletion.isResolved(), - "forceFlush should not resolve before logger.flush resolves", - ); - - deferred.resolve(); - await forceFlushCompletion.promise; - - assert.isTrue( - forceFlushCompletion.isResolved(), - "forceFlush should resolve after logger.flush", - ); - }); - - test("shutdown waits for logger.flush to complete", async () => { - let flushCalls = 0; - const deferred = createDeferredPromise(); - - const processor = new OpenAIAgentsTraceProcessor({ - logger: { - flush: () => { - flushCalls += 1; - return deferred.promise; - }, - } as any, - }); - - const shutdownCompletion = trackAsyncCompletion(processor.shutdown()); - - await Promise.resolve(); - - assert.equal( - flushCalls, - 1, - "shutdown should call logger.flush exactly once", - ); - assert.isFalse( - shutdownCompletion.isResolved(), - "shutdown should not resolve before logger.flush resolves", - ); - - deferred.resolve(); - await shutdownCompletion.promise; - - assert.isTrue( - shutdownCompletion.isResolved(), - "shutdown should resolve after logger.flush", - ); - }); -}); diff --git a/integrations/openai-agents-js/src/openai-agents-types.ts b/integrations/openai-agents-js/src/openai-agents-types.ts deleted file mode 100644 index 33a00a7d9..000000000 --- a/integrations/openai-agents-js/src/openai-agents-types.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Type definitions copied from @openai/agents-core. - * - * Original source: https://github.com/openai/openai-agents-js - * - * MIT License - * - * Copyright (c) 2025 OpenAI - * - * 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. - */ - -type SpanDataBase = { - type: string; -}; - -export type AgentSpanData = SpanDataBase & { - type: "agent"; - name: string; - handoffs?: string[]; - tools?: string[]; - output_type?: string; -}; - -export type FunctionSpanData = SpanDataBase & { - type: "function"; - name: string; - input: string; - output: string; - mcp_data?: string; -}; - -export type GenerationSpanData = SpanDataBase & { - type: "generation"; - input?: Array>; - output?: Array>; - model?: string; - model_config?: Record; - usage?: Record; -}; - -export type ResponseSpanData = SpanDataBase & { - type: "response"; - response_id?: string; - /** - * Not used by the OpenAI tracing provider but helpful for other tracing providers. - */ - _input?: string | Record[]; - _response?: Record; -}; - -export type HandoffSpanData = SpanDataBase & { - type: "handoff"; - from_agent?: string; - to_agent?: string; -}; - -export type CustomSpanData = SpanDataBase & { - type: "custom"; - name: string; - data: Record; -}; - -export type GuardrailSpanData = SpanDataBase & { - type: "guardrail"; - name: string; - triggered: boolean; -}; - -export type TranscriptionSpanData = SpanDataBase & { - type: "transcription"; - input: { - data: string; - format: "pcm" | string; - }; - output?: string; - model?: string; - model_config?: Record; -}; - -export type SpeechSpanData = SpanDataBase & { - type: "speech"; - input?: string; - output: { - data: string; - format: "pcm" | string; - }; - model?: string; - model_config?: Record; -}; - -export type SpeechGroupSpanData = SpanDataBase & { - type: "speech_group"; - input?: string; -}; - -export type MCPListToolsSpanData = SpanDataBase & { - type: "mcp_tools"; - server?: string; - result?: string[]; -}; - -export type SpanData = - | AgentSpanData - | FunctionSpanData - | GenerationSpanData - | ResponseSpanData - | HandoffSpanData - | CustomSpanData - | GuardrailSpanData - | TranscriptionSpanData - | SpeechSpanData - | SpeechGroupSpanData - | MCPListToolsSpanData; - -// Simplified versions of Trace and Span types - only including what we actually use -export type Trace = { - type: "trace"; - traceId: string; - name: string; - groupId: string | null; - metadata?: Record; -}; - -export type Span = { - type: "trace.span"; - traceId: string; - spanData: TData; - spanId: string; - parentId: string | null; - startedAt: string | null; - endedAt: string | null; - error: { message: string; data?: Record } | null; -}; diff --git a/integrations/openai-agents-js/src/test-logger.d.ts b/integrations/openai-agents-js/src/test-logger.d.ts deleted file mode 100644 index 1cc19256d..000000000 --- a/integrations/openai-agents-js/src/test-logger.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare module "@braintrust-test/logger" { - export interface TestBackgroundLogger { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - drain(): Promise; - } - - export const _exportsForTestingOnly: { - simulateLoginForTests(): Promise; - useTestBackgroundLogger(): TestBackgroundLogger; - clearTestBackgroundLogger(): void; - }; -} diff --git a/integrations/openai-agents-js/src/types.ts b/integrations/openai-agents-js/src/types.ts deleted file mode 100644 index 15aea9a6f..000000000 --- a/integrations/openai-agents-js/src/types.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Logger } from "braintrust"; -import type { - SpanData, - AgentSpanData, - FunctionSpanData, - GenerationSpanData, - ResponseSpanData, - HandoffSpanData, - CustomSpanData, - GuardrailSpanData, - MCPListToolsSpanData, - TranscriptionSpanData, - SpeechSpanData, - SpeechGroupSpanData, - Trace, - Span, -} from "./openai-agents-types"; - -export enum SpanType { - AGENT = "agent", - RESPONSE = "response", - FUNCTION = "function", - HANDOFF = "handoff", - GUARDRAIL = "guardrail", - GENERATION = "generation", - CUSTOM = "custom", - MCP_TOOLS = "mcp_tools", - TRANSCRIPTION = "transcription", - SPEECH = "speech", - SPEECH_GROUP = "speech_group", -} - -export type AgentsTrace = Trace; - -export type AgentsSpan = Span; - -export type SpanInput = - | string - | Array> - | Record[]; - -export type SpanOutput = - | string - | Array> - | Record; - -export type TraceMetadata = { - firstInput: SpanInput | null; - lastOutput: SpanOutput | null; -}; - -export interface OpenAIAgentsTraceProcessorOptions { - logger?: Logger; - maxTraces?: number; -} - -// Type guard functions -export function isResponseSpanData( - spanData: SpanData, -): spanData is ResponseSpanData { - return spanData.type === SpanType.RESPONSE; -} - -export function isGenerationSpanData( - spanData: SpanData, -): spanData is GenerationSpanData { - return spanData.type === SpanType.GENERATION; -} - -export function isAgentSpanData(spanData: SpanData): spanData is AgentSpanData { - return spanData.type === SpanType.AGENT; -} - -export function isFunctionSpanData( - spanData: SpanData, -): spanData is FunctionSpanData { - return spanData.type === SpanType.FUNCTION; -} - -export function isHandoffSpanData( - spanData: SpanData, -): spanData is HandoffSpanData { - return spanData.type === SpanType.HANDOFF; -} - -export function isGuardrailSpanData( - spanData: SpanData, -): spanData is GuardrailSpanData { - return spanData.type === SpanType.GUARDRAIL; -} - -export function isCustomSpanData( - spanData: SpanData, -): spanData is CustomSpanData { - return spanData.type === SpanType.CUSTOM; -} - -export function isMCPListToolsSpanData( - spanData: SpanData, -): spanData is MCPListToolsSpanData { - return spanData.type === SpanType.MCP_TOOLS; -} - -export function isTranscriptionSpanData( - spanData: SpanData, -): spanData is TranscriptionSpanData { - return spanData.type === SpanType.TRANSCRIPTION; -} - -export function isSpeechSpanData( - spanData: SpanData, -): spanData is SpeechSpanData { - return spanData.type === SpanType.SPEECH; -} - -export function isSpeechGroupSpanData( - spanData: SpanData, -): spanData is SpeechGroupSpanData { - return spanData.type === SpanType.SPEECH_GROUP; -} diff --git a/integrations/openai-agents-js/tsconfig.json b/integrations/openai-agents-js/tsconfig.json deleted file mode 100644 index 5fd07c79d..000000000 --- a/integrations/openai-agents-js/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "lib": ["es2022"], - "module": "commonjs", - "target": "es2022", - "moduleResolution": "node", - "strict": true, - "noUnusedLocals": true, - "esModuleInterop": true, - "skipLibCheck": true - }, - "include": ["src"], - "exclude": ["node_modules/**", "**/dist/**"] -} diff --git a/integrations/openai-agents-js/tsup.config.ts b/integrations/openai-agents-js/tsup.config.ts deleted file mode 100644 index 7d69621aa..000000000 --- a/integrations/openai-agents-js/tsup.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig([ - { - entry: ["src/index.ts"], - format: ["cjs", "esm"], - outDir: "dist", - dts: true, - }, -]); diff --git a/integrations/openai-agents-js/turbo.json b/integrations/openai-agents-js/turbo.json deleted file mode 100644 index 69a3e1241..000000000 --- a/integrations/openai-agents-js/turbo.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["//"], - "tasks": { - "build": { - "dependsOn": ["^build"], - "outputs": ["dist/**"] - }, - "test": { - "dependsOn": ["build"] - } - } -} diff --git a/integrations/openai-agents-js/vitest.config.ts b/integrations/openai-agents-js/vitest.config.ts deleted file mode 100644 index 1c14e5ef6..000000000 --- a/integrations/openai-agents-js/vitest.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig } from "vitest/config"; -import { fileURLToPath } from "node:url"; - -export default defineConfig({ - resolve: { - alias: { - "@braintrust-test/logger": fileURLToPath( - new URL("../../js/src/logger.ts", import.meta.url), - ), - braintrust: fileURLToPath( - new URL("../../js/src/node/index.ts", import.meta.url), - ), - }, - }, - test: { - reporters: ["default"], - include: ["tests/**/*.test.ts", "src/**/*.test.ts"], - // Add any specific test configuration if needed - }, -}); diff --git a/integrations/vercel-ai-sdk/CHANGELOG.md b/integrations/vercel-ai-sdk/CHANGELOG.md deleted file mode 100644 index 131233302..000000000 --- a/integrations/vercel-ai-sdk/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -# @braintrust/vercel-ai-sdk - -## 0.0.7 - -- `@braintrust/vercel-ai-sdk` is now deprecated. This release marks the last release for this package. (https://github.com/braintrustdata/braintrust-sdk-javascript/pull/2383) -- Updated dependencies: braintrust@3.29.0 - -### Migration - -To migrate manual instrumentation, install `braintrust` and use `wrapAISDK` -from `braintrust` as described in the -[Vercel AI SDK integration guide](https://www.braintrust.dev/docs/integrations/sdk-integrations/vercel). -Automatic AI SDK instrumentation is also available through the Braintrust -runtime hook and supported bundler integrations; see the -[auto-instrumentation guide](https://www.braintrust.dev/docs/instrument/trace-llm-calls#auto-instrumentation) -for setup instructions. The legacy `BraintrustAdapter` stream conversion -helpers have no direct replacement; migrate those usages to `BraintrustStream` -from `braintrust`, documented in the -[TypeScript SDK reference](https://www.braintrust.dev/docs/reference/libs/nodejs), -and the current AI SDK stream response APIs. - -## 0.0.6 - -### Patch Changes - -- Update the AI SDK dependency to a patched version and preserve stream adapter compatibility. (https://github.com/braintrustdata/braintrust-sdk-javascript/pull/1992) -- Updated dependencies: braintrust@3.11.0 - -## 0.0.5 - -### Patch Changes - -- Added `toDataStreamResponse()` as the preferred response helper for Braintrust streams. -- Kept `toAIStreamResponse()` as a deprecated alias for compatibility. -- Updated the adapter for compatibility with newer Zod versions. diff --git a/integrations/vercel-ai-sdk/README.md b/integrations/vercel-ai-sdk/README.md deleted file mode 100644 index 73e42908e..000000000 --- a/integrations/vercel-ai-sdk/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# @braintrust/vercel-ai-sdk - -> [!WARNING] -> This package and all of its exports are deprecated and will stop being -> published after the next release. Use the Vercel AI SDK integration in -> `braintrust` instead. - -For manual instrumentation, use `wrapAISDK` from `braintrust`: - -```ts -import * as ai from "ai"; -import { wrapAISDK } from "braintrust"; - -const { generateText, streamText } = wrapAISDK(ai); -``` - -Braintrust also supports automatic AI SDK instrumentation. See the -[Braintrust SDK documentation](../../js/README.md#auto-instrumentation) for -runtime-hook and bundler setup. - -The legacy `BraintrustAdapter` stream conversion helpers do not have a direct -replacement. Migrate those usages to `BraintrustStream` from `braintrust` and -the current AI SDK stream response APIs before this package stops being -published. diff --git a/integrations/vercel-ai-sdk/package.json b/integrations/vercel-ai-sdk/package.json deleted file mode 100644 index dcc0f2bf0..000000000 --- a/integrations/vercel-ai-sdk/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@braintrust/vercel-ai-sdk", - "version": "0.0.7", - "description": "SDK for integrating Braintrust with Vercel AI SDK", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "scripts": { - "build": "tsup", - "check:typings": "tsc --noEmit", - "watch": "tsup --watch", - "clean": "rm -r dist/*", - "test": "vitest run", - "yalc:publish": "yalc publish" - }, - "author": "Braintrust Data Inc.", - "license": "MIT", - "devDependencies": { - "@types/node": "^20.10.5", - "braintrust": "workspace:*", - "tsup": "^8.5.1", - "typescript": "^5.3.3", - "vitest": "4.1.5" - }, - "peerDependencies": { - "braintrust": ">=0.0.141" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", - "directory": "integrations/vercel-ai-sdk" - }, - "homepage": "https://www.braintrust.dev/docs", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/", - "provenance": true - } -} diff --git a/integrations/vercel-ai-sdk/src/adapter.test.ts b/integrations/vercel-ai-sdk/src/adapter.test.ts deleted file mode 100644 index 99fcb5ee7..000000000 --- a/integrations/vercel-ai-sdk/src/adapter.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { BraintrustStream } from "braintrust"; -import { ReadableStream, WritableStream } from "stream/web"; -import { expect, test } from "vitest"; -import { BraintrustAdapter } from "."; - -type BraintrustStreamChunk = - ReturnType extends ReadableStream< - infer Chunk - > - ? Chunk - : never; - -test("text callbacks", async () => { - const textStream = new ReadableStream({ - start(controller) { - controller.enqueue({ type: "text_delta", data: "Hello, " }); - controller.enqueue({ type: "text_delta", data: "world!" }); - controller.close(); - }, - }); - - const { started, completion, final, tokens, text } = - await streamWithCallbacks(textStream); - - expect(started).toBe(true); - expect(completion).toBe("Hello, world!"); - expect(final).toBe("Hello, world!"); - expect(tokens).toEqual(["Hello, ", "world!"]); - expect(text).toEqual(["Hello, ", "world!"]); -}); - -test("json callbacks", async () => { - const toolStream = new ReadableStream({ - start(controller) { - controller.enqueue({ type: "json_delta", data: `{"a":` }); - controller.enqueue({ type: "json_delta", data: ` 1}` }); - controller.close(); - }, - }); - - const { started, completion, final, tokens, text } = - await streamWithCallbacks(toolStream); - - expect(started).toBe(true); - expect(completion).toBe(null); - expect(final && JSON.parse(final)).toEqual({ a: 1 }); - expect(tokens).toEqual([]); - expect(text).toEqual([]); -}); - -async function streamWithCallbacks( - stream: ReadableStream, -) { - let started = false; - let completion: string | null = null; - let final: string | null = null; - let tokens: string[] = []; - let text: string[] = []; - - const vercelStream = BraintrustAdapter.toAIStream(stream, { - onStart: () => { - started = true; - }, - onCompletion: (c) => { - completion = c; - }, - onFinal: (f) => { - final = f; - }, - onToken: (t) => { - tokens.push(t); - }, - onText: (t) => { - text.push(t); - }, - }); - - await vercelStream.pipeTo( - new WritableStream({ - write(chunk) { - console.log(new TextDecoder().decode(chunk)); - }, - }), - ); - - return { started, completion, final, tokens, text }; -} diff --git a/integrations/vercel-ai-sdk/src/adapter.ts b/integrations/vercel-ai-sdk/src/adapter.ts deleted file mode 100644 index 4d9ec2e12..000000000 --- a/integrations/vercel-ai-sdk/src/adapter.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { BraintrustStream } from "braintrust"; - -/** - * @deprecated Use the AI SDK integration exported by `braintrust` instead. This package will stop being published after the next release. - */ -export interface AIStreamCallbacksAndOptions { - onStart?: () => void | Promise; - onCompletion?: (completion: string) => void | Promise; - onFinal?: (completion: string) => void | Promise; - onToken?: (token: string) => void | Promise; - onText?: (text: string) => void | Promise; -} - -function formatStreamPart(type: "text" | "data", value: unknown): string { - const code = type === "text" ? "0" : "2"; - return `${code}:${JSON.stringify(value)}\n`; -} - -import { ReadableStream, TransformStream } from "stream/web"; - -type BraintrustStreamChunk = - ReturnType extends ReadableStream< - infer Chunk - > - ? Chunk - : never; - -/** - * @deprecated Use `BraintrustStream` from `braintrust` with the current AI SDK response APIs instead. This package will stop being published after the next release. - */ -export type BraintrustStreamOrReadable = - | BraintrustStream - | ReadableStream - | ReadableStream - | ReadableStream; - -/** - * @deprecated Use `BraintrustStream` from `braintrust` with the current AI SDK stream APIs instead. This package will stop being published after the next release. - */ -export function toAIStream( - stream: BraintrustStreamOrReadable, - callbacks?: AIStreamCallbacksAndOptions, -): ReadableStream { - const btStream = - stream instanceof BraintrustStream - ? stream - : new BraintrustStream(stream as ReadableStream); - - return btStream - .toReadableStream() - .pipeThrough(btStreamToAISDKTransformStream(callbacks)); -} - -/** - * @deprecated Use `BraintrustStream` from `braintrust` with the current AI SDK response APIs instead. This package will stop being published after the next release. - */ -export function toDataStreamResponse( - stream: BraintrustStreamOrReadable, - init?: ResponseInit, -): Response { - return new Response(toAIStream(stream), { - headers: { - "Content-Type": "text/plain; charset=utf-8", - }, - ...init, - }); -} - -function btStreamToAISDKTransformStream( - callbacks?: AIStreamCallbacksAndOptions, -) { - const encoder = new TextEncoder(); - const jsonChunks: string[] = []; - const textChunks: string[] = []; - return new TransformStream({ - async start(controller) { - if (callbacks?.onStart) { - await callbacks.onStart(); - } - }, - async transform(chunk, controller) { - switch (chunk.type) { - case "text_delta": - controller.enqueue( - encoder.encode(formatStreamPart("text", chunk.data)), - ); - - // Call me old fashioned, but I think it's worth checking the existence of - // each function to avoid unnecessary context switches. - if (callbacks?.onToken) { - await callbacks.onToken(chunk.data); - } - if (callbacks?.onText) { - await callbacks.onText(chunk.data); - } - - if (callbacks?.onCompletion || callbacks?.onFinal) { - textChunks.push(chunk.data); - } - - break; - case "json_delta": - jsonChunks.push(chunk.data); - break; - } - }, - async flush(controller) { - if (jsonChunks.length > 0) { - const jsonString = jsonChunks.join(""); - const data = JSON.parse(jsonString); - controller.enqueue(encoder.encode(formatStreamPart("data", data))); - if (callbacks?.onFinal) { - await callbacks.onFinal(jsonString); - } - } else { - const textData = textChunks.join(""); - if (callbacks?.onCompletion) { - await callbacks.onCompletion(textData); - } - if (callbacks?.onFinal) { - await callbacks.onFinal(textData); - } - } - - controller.terminate(); - }, - }); -} diff --git a/integrations/vercel-ai-sdk/src/index.ts b/integrations/vercel-ai-sdk/src/index.ts deleted file mode 100644 index 55a019e33..000000000 --- a/integrations/vercel-ai-sdk/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as adapter from "./adapter"; - -/** - * @deprecated Use the AI SDK integration exported by `braintrust` instead. This package will stop being published after the next release. - */ -export const BraintrustAdapter = adapter; diff --git a/integrations/vercel-ai-sdk/tsconfig.json b/integrations/vercel-ai-sdk/tsconfig.json deleted file mode 100644 index ea6ad1861..000000000 --- a/integrations/vercel-ai-sdk/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "lib": ["es2022"], - "module": "commonjs", - "target": "es2022", - "moduleResolution": "node", - "strict": true, - "noUnusedLocals": true, - "esModuleInterop": true, - "skipLibCheck": true - }, - "include": ["."], - "exclude": ["node_modules/**", "**/dist/**"] -} diff --git a/integrations/vercel-ai-sdk/tsup.config.ts b/integrations/vercel-ai-sdk/tsup.config.ts deleted file mode 100644 index 7d69621aa..000000000 --- a/integrations/vercel-ai-sdk/tsup.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig([ - { - entry: ["src/index.ts"], - format: ["cjs", "esm"], - outDir: "dist", - dts: true, - }, -]); diff --git a/integrations/vercel-ai-sdk/turbo.json b/integrations/vercel-ai-sdk/turbo.json deleted file mode 100644 index 3e50dc890..000000000 --- a/integrations/vercel-ai-sdk/turbo.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": ["//"], - "tasks": { - "build": { - "outputs": ["**/dist/**"] - } - } -} diff --git a/integrations/vercel-ai-sdk/vitest.config.ts b/integrations/vercel-ai-sdk/vitest.config.ts deleted file mode 100644 index 744610c82..000000000 --- a/integrations/vercel-ai-sdk/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - reporters: ["default"], - include: ["tests/**/*.test.ts", "src/**/*.test.ts"], - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d709d6c8b..a36fd5d83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,9 +92,6 @@ importers: '@braintrust/deepseek-harness': specifier: workspace:^ version: link:../integrations/deepseek-harness - '@braintrust/langchain-js': - specifier: workspace:^ - version: link:../integrations/langchain-js '@braintrust/otel': specifier: workspace:^ version: link:../integrations/otel-js @@ -188,69 +185,6 @@ importers: specifier: 4.1.5 version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - integrations/langchain-js: - devDependencies: - '@langchain/anthropic': - specifier: ^1.3.1 - version: 1.3.5(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1)) - '@langchain/core': - specifier: ^1.1.6 - version: 1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1) - '@langchain/langgraph': - specifier: ^1.0.7 - version: 1.0.7(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@3.25.76))(zod@3.25.76) - '@langchain/openai': - specifier: ^1.2.0 - version: 1.2.1(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1))(ws@8.20.1) - '@types/node': - specifier: ^20.10.5 - version: 20.19.40 - braintrust: - specifier: workspace:* - version: link:../../js - msw: - specifier: ^2.6.6 - version: 2.13.6(@types/node@20.19.40)(typescript@5.9.3) - tsup: - specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vitest: - specifier: 4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - zod: - specifier: ^3.25.34 - version: 3.25.76 - zod-to-json-schema: - specifier: ^3.22.5 - version: 3.25.1(zod@3.25.76) - - integrations/openai-agents-js: - devDependencies: - '@openai/agents': - specifier: ^0.0.15 - version: 0.0.15(ws@8.20.1)(zod@3.25.76) - '@types/node': - specifier: ^20.10.5 - version: 20.19.40 - braintrust: - specifier: workspace:^ - version: link:../../js - tsup: - specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vitest: - specifier: 4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - zod: - specifier: ^3.25.34 - version: 3.25.76 - integrations/otel-js: dependencies: '@opentelemetry/api': @@ -343,24 +277,6 @@ importers: specifier: 4.1.5 version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - integrations/vercel-ai-sdk: - devDependencies: - '@types/node': - specifier: ^20.10.5 - version: 20.19.40 - braintrust: - specifier: workspace:* - version: link:../../js - tsup: - specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vitest: - specifier: 4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - js: dependencies: '@vercel/functions': @@ -562,15 +478,6 @@ packages: resolution: {integrity: sha512-9zu/TXaUy8BZhXedDtt1wT3H4LOlpKDO1/ftiFpeR3N1PCr3KJFKkxxlQWWt1NNp08xSwUNJ3JNY8yhl8av6eQ==} hasBin: true - '@anthropic-ai/sdk@0.71.2': - resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} @@ -613,9 +520,6 @@ packages: cpu: [x64] os: [win32] - '@cfworker/json-schema@4.0.3': - resolution: {integrity: sha512-ZykIcDTVv5UNmKWSTLAs3VukO6NDJkkSKxrgUTDPBkAlORVT3H9n5DbRjRl8xIotklscHdbLIa0b9+y3mQq73g==} - '@changesets/apply-release-plan@7.1.0': resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} @@ -1017,12 +921,6 @@ packages: engines: {node: '>=6'} hasBin: true - '@hono/node-server@1.19.17': - resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4.12.34 - '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1153,69 +1051,12 @@ packages: peerDependencies: tslib: '2' - '@langchain/anthropic@1.3.5': - resolution: {integrity: sha512-oB+sfcBujxW/E3cRhGfR5K23+TwZKPPZPjatEadQOZllyPoen0xefIuNoi0oLWfobz3/LPwoNixGU4si3oyGBQ==} - engines: {node: '>=20'} - peerDependencies: - '@langchain/core': 1.1.10 - - '@langchain/core@1.1.10': - resolution: {integrity: sha512-/G5Uo8EvoZ1AuNjCToMEtDMexbfvjqQ60AaF8nxN2klTgCUlPo83U0M9JP0yRQW6QzkPF5T3VwEik76WcbZ1vg==} - engines: {node: '>=20'} - - '@langchain/langgraph-checkpoint@1.0.0': - resolution: {integrity: sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==} - engines: {node: '>=18'} - peerDependencies: - '@langchain/core': ^1.0.1 - - '@langchain/langgraph-sdk@1.3.1': - resolution: {integrity: sha512-zTi7DZHwqtMEzapvm3I1FL4Q7OZsxtq9tTXy6s2gcCxyIU3sphqRboqytqVN7dNHLdTCLb8nXy49QKurs2MIBg==} - peerDependencies: - '@langchain/core': ^1.0.1 - react: ^18 || ^19 - react-dom: ^18 || ^19 - peerDependenciesMeta: - '@langchain/core': - optional: true - react: - optional: true - react-dom: - optional: true - - '@langchain/langgraph@1.0.7': - resolution: {integrity: sha512-EBGqNOWoRiEoLUaeuiXRpUM8/DE6QcwiirNyd97XhezStebBoTTilWH8CUt6S94JRGl5zwfBBRHfzotDnZS/eA==} - engines: {node: '>=18'} - peerDependencies: - '@langchain/core': ^1.0.1 - zod: ^3.25.32 || ^4.1.0 - zod-to-json-schema: ^3.x - peerDependenciesMeta: - zod-to-json-schema: - optional: true - - '@langchain/openai@1.2.1': - resolution: {integrity: sha512-eZYPhvXIwz0/8iCjj2LWqeaznQ7DZ6tBdvF+Ebv4sQW2UqJWZqRC8QIdKZgTbs8ffMWPHkSSOidYqu4XfWCNYg==} - engines: {node: '>=20'} - peerDependencies: - '@langchain/core': ^1.0.0 - '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@modelcontextprotocol/sdk@1.29.0': - resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@mswjs/interceptors@0.41.6': resolution: {integrity: sha512-qmDvJIjcNsZ6tXWy2G9yuCgMPTTn35GMA3dPpSLm7QJVpbQzYdw0ALy1bKoivXnEM3U93/OrK+/M719b+fg84Q==} engines: {node: '>=18'} @@ -1250,23 +1091,6 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@openai/agents-core@0.0.15': - resolution: {integrity: sha512-ODTqttjW0s0ejBe5PKnYRlFbJSZH2IO6OtUlRhIKmWiWrX6pGRxvpKjTSOXy8DEtpRHBj6Nhky0UoSlO6eOkDQ==} - peerDependencies: - zod: 3.25.40 - 3.25.67 - peerDependenciesMeta: - zod: - optional: true - - '@openai/agents-openai@0.0.15': - resolution: {integrity: sha512-YIX3n98HdmmWKkb/71OB+DCQUYyGEpqfzPjejzdtNLUvAEs3jvXf7nkC8oTISsuCwrirgBz0rQEefeo0oUlyFQ==} - - '@openai/agents-realtime@0.0.15': - resolution: {integrity: sha512-kSZzMyij9Xt3BpMb/9snuVnu7a5qKZLyhtN/kWMA+wmfETvWz23BBz6tbO5xOmurAt9//OktkB+94e0T0RBtlA==} - - '@openai/agents@0.0.15': - resolution: {integrity: sha512-B8y+WyWOeHowflPx09pyCfcqikC4OYWK27HTyNGt1oraXv93CzuamSr76iAaU1nWQ1MPbUwl6LHPX4BPUikVkQ==} - '@opentelemetry/api-logs@0.208.0': resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} engines: {node: '>=8.0.0'} @@ -1924,18 +1748,12 @@ packages: '@types/nunjucks@3.2.6': resolution: {integrity: sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==} - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.50.0': resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2090,10 +1908,6 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -2129,14 +1943,6 @@ packages: ajv: optional: true - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: @@ -2174,10 +1980,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -2212,9 +2014,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.14: resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true @@ -2223,10 +2022,6 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} - engines: {node: '>=18'} - brace-expansion@1.1.18: resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} @@ -2251,30 +2046,14 @@ packages: peerDependencies: esbuild: 0.28.1 - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - caniuse-lite@1.0.30001764: resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} @@ -2362,37 +2141,13 @@ packages: resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -2414,17 +2169,9 @@ packages: supports-color: optional: true - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2445,16 +2192,9 @@ packages: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -2467,10 +2207,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} @@ -2486,21 +2222,9 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -2510,9 +2234,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2618,17 +2339,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -2644,24 +2358,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.3.2: - resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -2713,10 +2413,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.8'} - find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -2744,14 +2440,6 @@ packages: engines: {node: '>=18.3.0'} hasBin: true - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2771,9 +2459,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2782,14 +2467,6 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} @@ -2834,10 +2511,6 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2854,14 +2527,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} - engines: {node: '>= 0.4'} - headers-polyfill@5.0.1: resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} @@ -2869,14 +2534,6 @@ packages: resolution: {integrity: sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==} engines: {node: '>=10.0.0'} - hono@4.13.5: - resolution: {integrity: sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==} - engines: {node: '>=16.9.0'} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -2928,14 +2585,6 @@ packages: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} engines: {node: '>=10'} - ip-address@10.4.0: - resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} - engines: {node: '>= 12'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2959,9 +2608,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -2984,16 +2630,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@6.2.2: - resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} - joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-tiktoken@1.0.21: - resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} - js-yaml@3.15.2: resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} hasBin: true @@ -3005,19 +2645,12 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-schema-to-ts@3.1.1: - resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} - engines: {node: '>=16'} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -3041,26 +2674,6 @@ packages: '@types/node': '>=18' typescript: '>=5.0.4 <7' - langsmith@0.7.1: - resolution: {integrity: sha512-Wjk90UjNoY5cBHMlNAC/eZx5clI8jnjBOBW8uJu8+MWBtx0QesNjsUiLtjI+I3UnrpxFFpDqGXcnhBjH654Mqg==} - 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 - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3201,21 +2814,9 @@ packages: engines: {node: '>= 12'} hasBin: true - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - memfs@4.51.1: resolution: {integrity: sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==} - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3235,10 +2836,6 @@ packages: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} - mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -3311,10 +2908,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -3359,17 +2952,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3377,18 +2962,6 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - openai@5.11.0: - resolution: {integrity: sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.23.8 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - openai@6.25.0: resolution: {integrity: sha512-mEh6VZ2ds2AGGokWARo18aPISI1OhlgdEIC1ewhkZr8pSIT31dec0ecr9Nhxx0JlybyOgoAT1sWeKtwPZzJyww==} hasBin: true @@ -3401,18 +2974,6 @@ packages: zod: optional: true - openai@6.32.0: - resolution: {integrity: sha512-j3k+BjydAf8yQlcOI7WUQMQTbbF5GEIMAE2iZYCOzwwB3S2pCheaWYp+XZRNAch4jWVc52PMDGRRjutao3lLCg==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3430,10 +2991,6 @@ packages: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -3454,18 +3011,6 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} - p-queue@6.6.2: - resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} - engines: {node: '>=8'} - - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - - p-timeout@3.2.0: - resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} - engines: {node: '>=8'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -3480,10 +3025,6 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3503,9 +3044,6 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.4.0: - resolution: {integrity: sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3537,10 +3075,6 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - pkce-challenge@5.0.0: - resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} - engines: {node: '>=16.20.0'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -3588,41 +3122,16 @@ packages: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} - engines: {node: '>=0.6'} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} - peerDependencies: - react: ^19.2.4 - - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} - engines: {node: '>=0.10.0'} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -3654,10 +3163,6 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - rettime@0.11.8: resolution: {integrity: sha512-0fERGXktJTyJ+h8fBEiPxHPEFOu0h15JY7JtwrOVqR5K+vb99ho6IyOo7ekLS3h4sJCzIDy4VWKIbZUfe9njmg==} @@ -3678,25 +3183,15 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - schema-utils@4.3.3: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} @@ -3709,20 +3204,9 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} - set-cookie-parser@3.1.0: resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3734,22 +3218,6 @@ packages: shiki@0.14.7: resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3981,10 +3449,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -4002,9 +3466,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -4069,10 +3530,6 @@ packages: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - typedoc-plugin-markdown@3.17.1: resolution: {integrity: sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==} peerDependencies: @@ -4121,10 +3578,6 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} @@ -4145,10 +3598,6 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vite-tsconfig-paths@4.3.2: resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} peerDependencies: @@ -4364,9 +3813,6 @@ packages: peerDependencies: zod: ^3.25 || ^4 - zod@3.25.67: - resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -4395,12 +3841,6 @@ snapshots: '@anthropic-ai/sdk@0.60.0': {} - '@anthropic-ai/sdk@0.71.2(zod@3.25.76)': - dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 3.25.76 - '@babel/runtime@7.28.4': {} '@braintrust/bt-darwin-arm64@0.12.0': @@ -4424,8 +3864,6 @@ snapshots: '@braintrust/bt-win32-x64@0.12.0': optional: true - '@cfworker/json-schema@4.0.3': {} - '@changesets/apply-release-plan@7.1.0': dependencies: '@changesets/config': 3.1.3 @@ -4849,11 +4287,6 @@ snapshots: protobufjs: 7.6.5 yargs: 17.7.2 - '@hono/node-server@1.19.17(hono@4.13.5)': - dependencies: - hono: 4.13.5 - optional: true - '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -4870,7 +4303,8 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/ansi@2.0.5': {} + '@inquirer/ansi@2.0.5': + optional: true '@inquirer/confirm@6.0.12(@types/node@20.19.40)': dependencies: @@ -4878,6 +4312,7 @@ snapshots: '@inquirer/type': 4.0.5(@types/node@20.19.40) optionalDependencies: '@types/node': 20.19.40 + optional: true '@inquirer/confirm@6.0.12(@types/node@22.19.1)': dependencies: @@ -4898,6 +4333,7 @@ snapshots: signal-exit: 4.1.0 optionalDependencies: '@types/node': 20.19.40 + optional: true '@inquirer/core@11.1.9(@types/node@22.19.1)': dependencies: @@ -4919,11 +4355,13 @@ snapshots: optionalDependencies: '@types/node': 24.12.4 - '@inquirer/figures@2.0.5': {} + '@inquirer/figures@2.0.5': + optional: true '@inquirer/type@4.0.5(@types/node@20.19.40)': optionalDependencies: '@types/node': 20.19.40 + optional: true '@inquirer/type@4.0.5(@types/node@22.19.1)': optionalDependencies: @@ -5005,68 +4443,6 @@ snapshots: '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) tslib: 2.8.1 - '@langchain/anthropic@1.3.5(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1))': - dependencies: - '@anthropic-ai/sdk': 0.71.2(zod@3.25.76) - '@langchain/core': 1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1) - zod: 3.25.76 - - '@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1)': - dependencies: - '@cfworker/json-schema': 4.0.3 - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.21 - langsmith: 0.7.1(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1) - mustache: 4.2.0 - p-queue: 6.6.2 - uuid: 11.1.1 - zod: 3.25.76 - transitivePeerDependencies: - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - openai - - ws - - '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1))': - dependencies: - '@langchain/core': 1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1) - uuid: 11.1.1 - - '@langchain/langgraph-sdk@1.3.1(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 11.1.1 - optionalDependencies: - '@langchain/core': 1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@langchain/langgraph@1.0.7(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@3.25.76))(zod@3.25.76)': - dependencies: - '@langchain/core': 1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1)) - '@langchain/langgraph-sdk': 1.3.1(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - uuid: 11.1.1 - zod: 3.25.76 - optionalDependencies: - zod-to-json-schema: 3.25.1(zod@3.25.76) - transitivePeerDependencies: - - react - - react-dom - - '@langchain/openai@1.2.1(@langchain/core@1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1))(ws@8.20.1)': - dependencies: - '@langchain/core': 1.1.10(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1) - js-tiktoken: 1.0.21 - openai: 6.25.0(ws@8.20.1)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - ws - '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -5083,29 +4459,6 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': - dependencies: - '@hono/node-server': 1.19.17(hono@4.13.5) - ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 8.3.2(express@5.2.1) - hono: 4.13.5 - jose: 6.2.2 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.0 - raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - transitivePeerDependencies: - - supports-color - optional: true - '@mswjs/interceptors@0.41.6': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -5114,6 +4467,7 @@ snapshots: is-node-process: 1.2.0 outvariant: 1.4.3 strict-event-emitter: 0.5.1 + optional: true '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: @@ -5134,70 +4488,20 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.16.0 - '@open-draft/deferred-promise@2.2.0': {} + '@open-draft/deferred-promise@2.2.0': + optional: true - '@open-draft/deferred-promise@3.0.0': {} + '@open-draft/deferred-promise@3.0.0': + optional: true '@open-draft/logger@0.3.0': dependencies: is-node-process: 1.2.0 outvariant: 1.4.3 + optional: true - '@open-draft/until@2.1.0': {} - - '@openai/agents-core@0.0.15(ws@8.20.1)(zod@3.25.76)': - dependencies: - '@openai/zod': zod@3.25.67 - debug: 4.4.3 - openai: 5.11.0(ws@8.20.1)(zod@3.25.76) - optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - '@cfworker/json-schema' - - supports-color - - ws - - '@openai/agents-openai@0.0.15(ws@8.20.1)(zod@3.25.76)': - dependencies: - '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.76) - '@openai/zod': zod@3.25.67 - debug: 4.4.3 - openai: 5.11.0(ws@8.20.1)(zod@3.25.76) - transitivePeerDependencies: - - '@cfworker/json-schema' - - supports-color - - ws - - zod - - '@openai/agents-realtime@0.0.15(zod@3.25.76)': - dependencies: - '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.76) - '@openai/zod': zod@3.25.67 - '@types/ws': 8.18.1 - debug: 4.4.3 - ws: 8.20.1 - transitivePeerDependencies: - - '@cfworker/json-schema' - - bufferutil - - supports-color - - utf-8-validate - - zod - - '@openai/agents@0.0.15(ws@8.20.1)(zod@3.25.76)': - dependencies: - '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.76) - '@openai/agents-openai': 0.0.15(ws@8.20.1)(zod@3.25.76) - '@openai/agents-realtime': 0.0.15(zod@3.25.76) - debug: 4.4.3 - openai: 5.11.0(ws@8.20.1)(zod@3.25.76) - transitivePeerDependencies: - - '@cfworker/json-schema' - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod + '@open-draft/until@2.1.0': + optional: true '@opentelemetry/api-logs@0.208.0': dependencies: @@ -5719,17 +5023,13 @@ snapshots: '@types/nunjucks@3.2.6': {} - '@types/retry@0.12.0': {} - '@types/set-cookie-parser@2.4.10': dependencies: '@types/node': 22.19.1 + optional: true - '@types/statuses@2.0.6': {} - - '@types/ws@8.18.1': - dependencies: - '@types/node': 22.19.1 + '@types/statuses@2.0.6': + optional: true '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4))(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4)': dependencies: @@ -5853,15 +5153,6 @@ snapshots: msw: 2.13.6(@types/node@20.19.40)(typescript@5.5.4) vite: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/mocker@4.1.5(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.5 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - msw: 2.13.6(@types/node@20.19.40)(typescript@5.9.3) - vite: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/mocker@4.1.5(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.5 @@ -5990,12 +5281,6 @@ snapshots: dependencies: event-target-shim: 5.0.1 - accepts@2.0.0: - dependencies: - mime-types: 3.0.1 - negotiator: 1.0.0 - optional: true - acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -6022,11 +5307,6 @@ snapshots: optionalDependencies: ajv: 8.20.0 - ajv-formats@3.0.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - optional: true - ajv-keywords@5.1.0(ajv@8.20.0): dependencies: ajv: 8.20.0 @@ -6064,8 +5344,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} any-promise@1.3.0: {} @@ -6088,29 +5366,12 @@ snapshots: balanced-match@1.0.2: {} - base64-js@1.5.1: {} - baseline-browser-mapping@2.9.14: {} better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 - body-parser@2.3.0: - dependencies: - bytes: 3.1.2 - content-type: 2.0.0 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.15.2 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - optional: true - brace-expansion@1.1.18: dependencies: balanced-match: 1.0.2 @@ -6139,27 +5400,10 @@ snapshots: esbuild: 0.28.1 load-tsconfig: 0.2.5 - bytes@3.1.2: - optional: true - cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - optional: true - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - optional: true - callsites@3.1.0: {} - camelcase@6.3.0: {} - caniuse-lite@1.0.30001764: {} chai@6.2.2: {} @@ -6192,7 +5436,8 @@ snapshots: slice-ansi: 7.1.2 string-width: 8.2.0 - cli-width@4.1.0: {} + cli-width@4.1.0: + optional: true cliui@7.0.4: dependencies: @@ -6228,31 +5473,9 @@ snapshots: consola@3.4.0: {} - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 - optional: true - - content-type@1.0.5: - optional: true - - content-type@2.0.0: - optional: true - convert-source-map@2.0.0: {} - cookie-signature@1.2.2: - optional: true - - cookie@0.7.2: - optional: true - - cookie@1.1.1: {} - - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 + cookie@1.1.1: optional: true cross-env@7.0.3: @@ -6271,13 +5494,8 @@ snapshots: dependencies: ms: 2.1.3 - decamelize@1.2.0: {} - deep-is@0.1.4: {} - depd@2.0.0: - optional: true - detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -6290,18 +5508,8 @@ snapshots: dotenv@17.3.1: {} - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - optional: true - eastasianwidth@0.2.0: {} - ee-first@1.1.1: - optional: true - electron-to-chromium@1.5.267: {} emoji-regex@10.6.0: {} @@ -6310,9 +5518,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@2.0.0: - optional: true - encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -6330,19 +5535,8 @@ snapshots: environment@1.1.0: {} - es-define-property@1.0.1: - optional: true - - es-errors@1.3.0: - optional: true - es-module-lexer@2.1.0: {} - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - optional: true - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -6374,9 +5568,6 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: - optional: true - escape-string-regexp@4.0.0: {} eslint-compat-utils@0.5.1(eslint@9.39.4(jiti@2.6.1)): @@ -6503,13 +5694,8 @@ snapshots: esutils@2.0.3: {} - etag@1.8.1: - optional: true - event-target-shim@5.0.1: {} - eventemitter3@4.0.7: {} - eventemitter3@5.0.4: {} events@3.3.0: {} @@ -6518,53 +5704,8 @@ snapshots: eventsource-parser@3.0.6: {} - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.0.6 - optional: true - expect-type@1.3.0: {} - express-rate-limit@8.3.2(express@5.2.1): - dependencies: - express: 5.2.1 - ip-address: 10.4.0 - optional: true - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.3.0 - content-disposition: 1.0.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.0 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.1 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.2 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - optional: true - extendable-error@0.1.7: {} fast-deep-equal@3.1.3: {} @@ -6613,18 +5754,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - optional: true - find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -6657,12 +5786,6 @@ snapshots: dependencies: fd-package-json: 2.0.0 - forwarded@0.2.0: - optional: true - - fresh@2.0.0: - optional: true - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -6682,33 +5805,10 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: - optional: true - get-caller-file@2.0.5: {} get-east-asian-width@1.5.0: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.3 - math-intrinsics: 1.1.0 - optional: true - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - optional: true - get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -6760,12 +5860,10 @@ snapshots: globrex@0.1.2: {} - gopd@1.2.0: - optional: true - graceful-fs@4.2.11: {} - graphql@16.13.2: {} + graphql@16.13.2: + optional: true handlebars@4.7.9: dependencies: @@ -6778,32 +5876,13 @@ snapshots: has-flag@4.0.0: {} - has-symbols@1.1.0: - optional: true - - hasown@2.0.3: - dependencies: - function-bind: 1.1.2 - optional: true - headers-polyfill@5.0.1: dependencies: '@types/set-cookie-parser': 2.4.10 set-cookie-parser: 3.1.0 - - heap-js@2.7.1: {} - - hono@4.13.5: optional: true - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - optional: true + heap-js@2.7.1: {} human-id@4.1.3: {} @@ -6843,12 +5922,6 @@ snapshots: ini@2.0.0: {} - ip-address@10.4.0: - optional: true - - ipaddr.js@1.9.1: - optional: true - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -6861,13 +5934,11 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-node-process@1.2.0: {} + is-node-process@1.2.0: + optional: true is-number@7.0.0: {} - is-promise@4.0.0: - optional: true - is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -6890,15 +5961,8 @@ snapshots: jiti@2.6.1: {} - jose@6.2.2: - optional: true - joycon@3.1.1: {} - js-tiktoken@1.0.21: - dependencies: - base64-js: 1.5.1 - js-yaml@3.15.2: dependencies: argparse: 1.0.10 @@ -6910,18 +5974,10 @@ snapshots: json-buffer@3.0.1: {} - json-schema-to-ts@3.1.1: - dependencies: - '@babel/runtime': 7.28.4 - ts-algebra: 2.0.0 - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} - json-schema-typed@8.0.2: - optional: true - json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -6956,15 +6012,6 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - langsmith@0.7.1(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(openai@6.32.0(ws@8.20.1)(zod@3.25.76))(ws@8.20.1): - dependencies: - p-queue: 6.6.2 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) - openai: 6.32.0(ws@8.20.1)(zod@3.25.76) - ws: 8.20.1 - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -7080,12 +6127,6 @@ snapshots: marked@4.3.0: {} - math-intrinsics@1.1.0: - optional: true - - media-typer@1.1.0: - optional: true - memfs@4.51.1: dependencies: '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) @@ -7095,9 +6136,6 @@ snapshots: tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 - merge-descriptors@2.0.0: - optional: true - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -7111,11 +6149,6 @@ snapshots: mime-db@1.54.0: {} - mime-types@3.0.1: - dependencies: - mime-db: 1.54.0 - optional: true - mimic-function@5.0.1: {} minimatch@3.1.5: @@ -7201,31 +6234,6 @@ snapshots: - '@types/node' optional: true - msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3): - dependencies: - '@inquirer/confirm': 6.0.12(@types/node@20.19.40) - '@mswjs/interceptors': 0.41.6 - '@open-draft/deferred-promise': 3.0.0 - '@types/statuses': 2.0.6 - cookie: 1.1.1 - graphql: 16.13.2 - headers-polyfill: 5.0.1 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - rettime: 0.11.8 - statuses: 2.0.2 - strict-event-emitter: 0.5.1 - tough-cookie: 6.0.1 - type-fest: 5.6.0 - until-async: 3.0.2 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@types/node' - msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4): dependencies: '@inquirer/confirm': 6.0.12(@types/node@22.19.1) @@ -7280,7 +6288,8 @@ snapshots: mustache@4.2.0: {} - mute-stream@3.0.0: {} + mute-stream@3.0.0: + optional: true mz@2.7.0: dependencies: @@ -7294,9 +6303,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@1.0.0: - optional: true - neo-async@2.6.2: {} nexus-rpc@0.0.1: {} @@ -7330,16 +6336,8 @@ snapshots: object-assign@4.1.1: {} - object-inspect@1.13.4: - optional: true - obug@2.1.1: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - optional: true - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -7348,22 +6346,11 @@ snapshots: dependencies: mimic-function: 5.0.1 - openai@5.11.0(ws@8.20.1)(zod@3.25.76): - optionalDependencies: - ws: 8.20.1 - zod: 3.25.76 - openai@6.25.0(ws@8.20.1)(zod@3.25.76): optionalDependencies: ws: 8.20.1 zod: 3.25.76 - openai@6.32.0(ws@8.20.1)(zod@3.25.76): - optionalDependencies: - ws: 8.20.1 - zod: 3.25.76 - optional: true - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7375,7 +6362,8 @@ snapshots: outdent@0.5.0: {} - outvariant@1.4.3: {} + outvariant@1.4.3: + optional: true oxc-resolver@11.19.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): optionalDependencies: @@ -7407,8 +6395,6 @@ snapshots: dependencies: p-map: 2.1.0 - p-finally@1.0.0: {} - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -7427,20 +6413,6 @@ snapshots: p-map@2.1.0: {} - p-queue@6.6.2: - dependencies: - eventemitter3: 4.0.7 - p-timeout: 3.2.0 - - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - - p-timeout@3.2.0: - dependencies: - p-finally: 1.0.0 - p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -7453,9 +6425,6 @@ snapshots: dependencies: callsites: 3.1.0 - parseurl@1.3.3: - optional: true - path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -7467,9 +6436,7 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 - path-to-regexp@6.3.0: {} - - path-to-regexp@8.4.0: + path-to-regexp@6.3.0: optional: true path-type@4.0.0: {} @@ -7488,9 +6455,6 @@ snapshots: pirates@4.0.6: {} - pkce-challenge@5.0.0: - optional: true - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -7536,43 +6500,12 @@ snapshots: '@types/node': 22.19.1 long: 5.3.2 - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - optional: true - punycode@2.3.1: {} - qs@6.15.2: - dependencies: - side-channel: 1.1.0 - optional: true - quansync@0.2.11: {} queue-microtask@1.2.3: {} - range-parser@1.2.1: - optional: true - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - optional: true - - react-dom@19.2.4(react@19.2.4): - dependencies: - react: 19.2.4 - scheduler: 0.27.0 - optional: true - - react@19.2.4: - optional: true - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -7597,9 +6530,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - retry@0.13.1: {} - - rettime@0.11.8: {} + rettime@0.11.8: + optional: true reusify@1.0.4: {} @@ -7657,17 +6589,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.3 fsevents: 2.3.3 - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.0 - transitivePeerDependencies: - - supports-color - optional: true - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7676,14 +6597,8 @@ snapshots: dependencies: tslib: 2.8.1 - safe-buffer@5.2.1: - optional: true - safer-buffer@2.1.2: {} - scheduler@0.27.0: - optional: true - schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 @@ -7695,36 +6610,7 @@ snapshots: semver@7.8.0: {} - send@1.2.0: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.1 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - optional: true - - serve-static@2.2.0: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.0 - transitivePeerDependencies: - - supports-color - optional: true - - set-cookie-parser@3.1.0: {} - - setprototypeof@1.2.0: + set-cookie-parser@3.1.0: optional: true shebang-command@2.0.0: @@ -7740,38 +6626,6 @@ snapshots: vscode-oniguruma: 1.7.0 vscode-textmate: 8.0.0 - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - optional: true - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - optional: true - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - optional: true - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - optional: true - siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -7813,11 +6667,13 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.2: {} + statuses@2.0.2: + optional: true std-env@4.1.0: {} - strict-event-emitter@0.5.1: {} + strict-event-emitter@0.5.1: + optional: true string-argv@0.3.2: {} @@ -7882,7 +6738,8 @@ snapshots: '@swc/counter': 0.1.3 webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26) - tagged-tag@1.0.0: {} + tagged-tag@1.0.0: + optional: true tapable@2.3.3: {} @@ -7942,22 +6799,22 @@ snapshots: tinyrainbow@3.1.0: {} - tldts-core@7.0.29: {} + tldts-core@7.0.29: + optional: true tldts@7.0.29: dependencies: tldts-core: 7.0.29 + optional: true to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - toidentifier@1.0.1: - optional: true - tough-cookie@6.0.1: dependencies: tldts: 7.0.29 + optional: true tr46@0.0.3: {} @@ -7967,8 +6824,6 @@ snapshots: tree-kill@1.2.2: {} - ts-algebra@2.0.0: {} - ts-api-utils@2.1.0(typescript@5.4.4): dependencies: typescript: 5.4.4 @@ -8096,12 +6951,6 @@ snapshots: type-fest@5.6.0: dependencies: tagged-tag: 1.0.0 - - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.1 optional: true typedoc-plugin-markdown@3.17.1(typedoc@0.25.13(typescript@5.4.4)): @@ -8138,9 +6987,6 @@ snapshots: universalify@0.1.2: {} - unpipe@1.0.0: - optional: true - unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -8148,7 +6994,8 @@ snapshots: picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 - until-async@3.0.2: {} + until-async@3.0.2: + optional: true update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: @@ -8162,9 +7009,6 @@ snapshots: uuid@11.1.1: {} - vary@1.1.2: - optional: true - vite-tsconfig-paths@4.3.2(typescript@5.4.4)(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): dependencies: debug: 4.4.3 @@ -8264,34 +7108,6 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.1.2 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 20.19.40 - transitivePeerDependencies: - - msw - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.5 @@ -8443,7 +7259,8 @@ snapshots: wrappy@1.0.2: {} - ws@8.20.1: {} + ws@8.20.1: + optional: true y18n@5.0.8: {} @@ -8492,8 +7309,6 @@ snapshots: dependencies: zod: 3.25.76 - zod@3.25.67: {} - zod@3.25.76: {} zod@4.3.6: {} diff --git a/scripts/release/_shared.mjs b/scripts/release/_shared.mjs index 2af80e54c..a37f5d6d3 100644 --- a/scripts/release/_shared.mjs +++ b/scripts/release/_shared.mjs @@ -19,18 +19,12 @@ export const PUBLISHABLE_PACKAGES = [ dir: "integrations/deepseek-harness", name: "@braintrust/deepseek-harness", }, - { dir: "integrations/langchain-js", name: "@braintrust/langchain-js" }, - { dir: "integrations/openai-agents-js", name: "@braintrust/openai-agents" }, { dir: "integrations/otel-js", name: "@braintrust/otel" }, { dir: "integrations/templates-nunjucks", name: "@braintrust/templates-nunjucks-js", }, { dir: "integrations/temporal-js", name: "@braintrust/temporal" }, - { - dir: "integrations/vercel-ai-sdk", - name: "@braintrust/vercel-ai-sdk", - }, ]; export const PRIVATE_WORKSPACE_PACKAGES = [ From d3daacd39d32a940898ec559328c018dfd39aea1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 23 Sep 2026 16:55:41 -0400 Subject: [PATCH 11/19] ref!: Simplify Eval type definitions (#2516) Ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) --- .changeset/simplify-eval-types.md | 5 +++++ js/src/framework.ts | 21 ++++++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 .changeset/simplify-eval-types.md diff --git a/.changeset/simplify-eval-types.md b/.changeset/simplify-eval-types.md new file mode 100644 index 000000000..2805f7950 --- /dev/null +++ b/.changeset/simplify-eval-types.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref!: Simplify Eval type definitions diff --git a/js/src/framework.ts b/js/src/framework.ts index 9d2bef9d3..bce815f8f 100644 --- a/js/src/framework.ts +++ b/js/src/framework.ts @@ -97,13 +97,13 @@ export type EvalData< Metadata extends BaseMetadata = DefaultMetadataType, > = | EvalCase[] - | (() => EvalCase[]) | Promise[]> - | (() => Promise[]>) - | AsyncGenerator> | AsyncIterable> | BaseExperiment - | (() => BaseExperiment); + | (() => + | EvalCase[] + | Promise[]> + | BaseExperiment); export type EvalTask< Input, @@ -111,15 +111,10 @@ export type EvalTask< Expected, Metadata extends BaseMetadata, Parameters extends EvalParameters, -> = - | (( - input: Input, - hooks: EvalHooks, - ) => Promise) - | (( - input: Input, - hooks: EvalHooks, - ) => Output); +> = ( + input: Input, + hooks: EvalHooks, +) => Output | Promise; type TaskProgressEvent = Omit< SSEProgressEventData, From 9dc521251a1399b1ec292d9086df6c4468c0c75d Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 24 Sep 2026 12:35:47 -0400 Subject: [PATCH 12/19] ref!: Don't make generated types part of public api (#2517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref [SDK-226](https://linear.app/braintrustdata/issue/SDK-226/reduce-zod-derived-types-in-public-typescript-sdk-declarations) - Replace broad, backend-generated SDK types with smaller, explicit contracts, reducing the type complexity users’ editors and TypeScript compilers must process. - Bundle only the runtime validators the SDK needs. Previously, importing generated schemas also pulled unrelated backend validators into client bundles. - Simplify prompt and function types and remove internal cache-serialization methods from the public API, while preserving supported inputs and autocomplete. - Keep generated definitions as compatibility references, with tests checking type compatibility, validation behavior, autocomplete, and their exclusion from production bundles. All 1,536 replay tests pass. --- .changeset/simplify-public-sdk-types.md | 5 + e2e/helpers/mock-braintrust-server.test.ts | 61 ++ e2e/helpers/mock-braintrust-server.ts | 53 +- e2e/scripts/run-e2e-tests.mjs | 2 +- js/eslint.config.ts | 24 + js/src/eval-parameters.ts | 2 +- js/src/framework-types.ts | 2 +- js/src/framework.ts | 12 +- js/src/framework2.ts | 43 +- js/src/functions/invoke.ts | 12 +- js/src/functions/stream.ts | 14 +- js/src/gitutil.ts | 5 +- js/src/isomorph.ts | 5 +- js/src/logger.ts | 188 +++--- js/src/prompt-cache/parameters-cache.ts | 10 +- js/src/prompt-cache/prompt-cache.ts | 10 +- js/src/prompt-schemas.ts | 16 +- js/src/prompt-types.test.ts | 135 +++++ js/src/public-types.test.ts | 2 + js/src/sandbox.ts | 4 +- js/src/sdk-contracts.test.ts | 180 ++++++ js/src/sdk-schemas.ts | 573 ++++++++++++++++++ js/src/sdk-types.ts | 573 ++++++++++++++++++ .../api-compatibility.test.ts | 16 + js/util/git_fields.ts | 2 +- js/util/object.ts | 9 +- js/util/span_identifier_v4.ts | 2 +- 27 files changed, 1798 insertions(+), 162 deletions(-) create mode 100644 .changeset/simplify-public-sdk-types.md create mode 100644 e2e/helpers/mock-braintrust-server.test.ts create mode 100644 js/src/prompt-types.test.ts create mode 100644 js/src/sdk-contracts.test.ts create mode 100644 js/src/sdk-schemas.ts create mode 100644 js/src/sdk-types.ts diff --git a/.changeset/simplify-public-sdk-types.md b/.changeset/simplify-public-sdk-types.md new file mode 100644 index 000000000..0cff8f061 --- /dev/null +++ b/.changeset/simplify-public-sdk-types.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref!: Don't make generated types part of public api diff --git a/e2e/helpers/mock-braintrust-server.test.ts b/e2e/helpers/mock-braintrust-server.test.ts new file mode 100644 index 000000000..4ba4af9d1 --- /dev/null +++ b/e2e/helpers/mock-braintrust-server.test.ts @@ -0,0 +1,61 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { startMockBraintrustServer } from "./mock-braintrust-server"; + +afterEach(() => vi.restoreAllMocks()); + +it.each([ + { path: "/logs3", statuses: [502, 503, 504, 200], succeeds: true }, + { path: "/logs3", statuses: [502, 502, 502, 502], succeeds: false }, + { path: "/logs3", statuses: [400], succeeds: false }, + { path: "/api/project/register", statuses: [502], succeeds: false }, +])( + "forwards $path with responses $statuses", + async ({ path, statuses, succeeds }) => { + const realFetch = globalThis.fetch; + const forward = vi.spyOn(globalThis, "fetch"); + for (const status of statuses) { + forward.mockResolvedValueOnce(new Response("{}", { status })); + } + const server = await startMockBraintrustServer({ + prodForwarding: { + apiKey: "test-forwarding-key", + apiUrl: "https://forwarding.invalid", + appUrl: "https://forwarding.invalid", + orgId: "test-org", + orgName: "test-org", + projectId: "test-project", + projectName: "tmp-luca-forwarding-test", + }, + }); + const body = JSON.stringify({ + api_version: 2, + rows: [{ id: "test-row", project_id: "test-project" }], + }); + try { + const response = await realFetch(`${server.url}${path}`, { + method: "POST", + body, + }); + await response.text(); + } finally { + if (succeeds) { + await server.close(); + } else { + await expect(server.close()).rejects.toThrow( + `prodForwarding failed for POST ${path}: ${statuses.at(-1)}`, + ); + } + } + + expect(forward).toHaveBeenCalledTimes(statuses.length); + for (const [url, init] of forward.mock.calls) { + expect(String(url)).toBe(`https://forwarding.invalid${path}`); + expect(init?.body).toBe(body); + expect(new Headers(init?.headers).get("authorization")).toBe( + "Bearer test-forwarding-key", + ); + } + expect(server.requests).toHaveLength(1); + expect(server.payloads).toHaveLength(path === "/logs3" ? 1 : 0); + }, +); diff --git a/e2e/helpers/mock-braintrust-server.ts b/e2e/helpers/mock-braintrust-server.ts index 7f59a57ed..78e072e15 100644 --- a/e2e/helpers/mock-braintrust-server.ts +++ b/e2e/helpers/mock-braintrust-server.ts @@ -6,6 +6,7 @@ import type { ServerResponse, } from "node:http"; import type { AddressInfo } from "node:net"; +import { setTimeout as sleep } from "node:timers/promises"; import type { ProdForwarding } from "./prod-forwarding"; export type JsonValue = @@ -472,29 +473,41 @@ export async function startMockBraintrustServer( } headers.set("authorization", `Bearer ${prodForwarding.apiKey}`); - const response = await fetch(url, { - body: - prodRequest.method === "GET" || prodRequest.method === "HEAD" - ? undefined - : prodRequest.rawBody, - headers, - method: prodRequest.method, - }); + // Log rows have stable IDs, so retrying an upload does not duplicate spans. + // Keep registration and other requests single-attempt. + const maxAttempts = prodRequest.path === "/logs3" ? 4 : 1; + for (let attempt = 1; ; attempt++) { + const response = await fetch(url, { + body: + prodRequest.method === "GET" || prodRequest.method === "HEAD" + ? undefined + : prodRequest.rawBody, + headers, + method: prodRequest.method, + }); - if (!response.ok) { - const responseText = await response.text().catch(() => ""); - throw new Error( - `prodForwarding failed for ${capturedRequest.method} ${capturedRequest.path}: ${response.status} ${response.statusText}${ - responseText ? `: ${responseText.slice(0, 500)}` : "" - }`, - ); - } + if (!response.ok) { + const responseText = await response.text().catch(() => ""); + if ( + attempt < maxAttempts && + [502, 503, 504].includes(response.status) + ) { + await sleep(500 * 2 ** (attempt - 1)); + continue; + } + throw new Error( + `prodForwarding failed for ${capturedRequest.method} ${capturedRequest.path}: ${response.status} ${response.statusText}${ + responseText ? `: ${responseText.slice(0, 500)}` : "" + }`, + ); + } - if (options.drainResponseBody) { - await response.arrayBuffer(); - } + if (options.drainResponseBody) { + await response.arrayBuffer(); + } - return response; + return response; + } } const server = createServer((req, res) => { diff --git a/e2e/scripts/run-e2e-tests.mjs b/e2e/scripts/run-e2e-tests.mjs index b4254e6b7..5b53a6f3d 100644 --- a/e2e/scripts/run-e2e-tests.mjs +++ b/e2e/scripts/run-e2e-tests.mjs @@ -22,7 +22,7 @@ const scenarioArgs = rawArgs.filter((arg) => arg !== "--update"); const testTargets = scenarioArgs.length > 0 ? scenarioArgs.map((arg) => scenarioPathArg(arg)) - : await defaultScenarioTestPaths(); + : [...(await defaultScenarioTestPaths()), "helpers"]; const vitestArgs = [ "run", "--run", diff --git a/js/eslint.config.ts b/js/eslint.config.ts index 80829409c..00dad5c0a 100644 --- a/js/eslint.config.ts +++ b/js/eslint.config.ts @@ -20,6 +20,17 @@ const entryFiles = tsupConfig.flatMap((config) => { return entries; }); +const generatedTypeImports = { + group: [ + "**/generated_types", + "**/generated_plain_types", + "**/generated_types.ts", + "**/generated_plain_types.ts", + ], + message: + "Generated backend definitions are compatibility-test fixtures. Use SDK-owned types and validators instead.", +}; + export default [ { ignores: [ @@ -161,6 +172,18 @@ export default [ "no-restricted-properties": "off", }, }, + { + files: ["src/**/*.ts", "src/**/*.tsx", "util/**/*.ts"], + languageOptions: { + parser: tsparser, + }, + plugins: { + "@typescript-eslint": tseslint, + }, + rules: { + "no-restricted-imports": ["error", { patterns: [generatedTypeImports] }], + }, + }, { files: ["src/**/*.ts", "src/**/*.tsx"], ignores: [...entryFiles, "**/*.test.ts", "**/*.test.tsx"], @@ -169,6 +192,7 @@ export default [ "error", { patterns: [ + generatedTypeImports, { group: [ "./exports", diff --git a/js/src/eval-parameters.ts b/js/src/eval-parameters.ts index bc15dc105..9a21208ad 100644 --- a/js/src/eval-parameters.ts +++ b/js/src/eval-parameters.ts @@ -5,7 +5,7 @@ import { promptDefinitionToPromptData, type PromptDefinitionWithTools, } from "./prompt-schemas"; -import { PromptData as promptDataSchema } from "./generated_types"; +import { PromptData as promptDataSchema } from "./sdk-schemas"; export type EvalParameters = Record< string, diff --git a/js/src/framework-types.ts b/js/src/framework-types.ts index 0f9d45c26..27279c8b0 100644 --- a/js/src/framework-types.ts +++ b/js/src/framework-types.ts @@ -1,4 +1,4 @@ -import type { IfExistsType as IfExists } from "./generated_plain_types"; +import type { IfExists } from "./sdk-types"; export type GenericFunction = | ((input: Input) => Output) diff --git a/js/src/framework.ts b/js/src/framework.ts index bce815f8f..2e7fea418 100644 --- a/js/src/framework.ts +++ b/js/src/framework.ts @@ -7,13 +7,13 @@ import { SpanTypeAttribute, spanObjectTypeV3ToTypedString, } from "../util/index"; -import { ObjectReference as ObjectReferenceSchema } from "./generated_types"; +import { ObjectReference as ObjectReferenceSchema } from "./sdk-schemas"; import type { - GitMetadataSettingsType as GitMetadataSettings, - ObjectReferenceType as ObjectReference, - RepoInfoType as RepoInfo, - SSEProgressEventDataType as SSEProgressEventData, -} from "./generated_plain_types"; + GitMetadataSettings, + ObjectReference, + RepoInfo, + SSEProgressEventData, +} from "./sdk-types"; import { queue } from "async"; import iso from "./isomorph"; diff --git a/js/src/framework2.ts b/js/src/framework2.ts index ed1eef15f..fb9ee26ea 100644 --- a/js/src/framework2.ts +++ b/js/src/framework2.ts @@ -3,17 +3,16 @@ import type { Trace } from "./trace"; import iso from "./isomorph"; import { slugify } from "../util/string_util"; import { z } from "zod/v3"; -import { Project as projectSchema } from "./generated_types"; +import { Project as projectSchema } from "./sdk-schemas"; import type { - FunctionTypeEnumType as FunctionType, - IfExistsType as IfExists, - SavedFunctionIdType as SavedFunctionId, - PromptBlockDataType as PromptBlockData, - PromptDataType as PromptData, - ToolFunctionDefinitionType as ToolFunctionDefinition, - ExtendedSavedFunctionIdType as ExtendedSavedFunctionId, - FunctionDataType, -} from "./generated_plain_types"; + FunctionTypeEnum as FunctionType, + IfExists, + SavedFunctionId, + PromptBlockData, + PromptData, + ToolFunctionDefinition, + ExtendedSavedFunctionId, +} from "./sdk-types"; import { loadPrettyXact, TransactionId } from "../util/index"; import { _internalGetGlobalState, @@ -121,7 +120,7 @@ class Project { } await login(); const projectMap = new ProjectNameIdMap(); - const functionDefinitions: FunctionEvent[] = []; + const functionDefinitions: PromptFunctionEvent[] = []; if (this._publishableCodeFunctions.length > 0) { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.warn( @@ -499,7 +498,7 @@ export class CodePrompt { async toFunctionDefinition( projectNameToId: ProjectNameIdMap, - ): Promise { + ): Promise { const prompt_data = { ...this.prompt, }; @@ -666,7 +665,7 @@ export class CodeParameters { async toFunctionDefinition( projectNameToId: ProjectNameIdMap, - ): Promise { + ): Promise { const schema = serializeEvalParameterstoParametersSchema(this.schema); return { project_id: await projectNameToId.resolve(this.project), @@ -776,13 +775,11 @@ function getDefaultDataFromParametersSchema( ); } -interface FunctionEvent { +interface FunctionEventBase { project_id: string; slug: string; name: string; description: string; - prompt_data?: PromptData; - function_data: FunctionDataType; function_type?: FunctionType; if_exists?: IfExists; tags?: string[]; @@ -790,6 +787,20 @@ interface FunctionEvent { environments?: { slug: string }[]; } +interface PromptFunctionEvent extends FunctionEventBase { + prompt_data: PromptData; + function_data: { type: "prompt" }; +} + +interface ParametersFunctionEvent extends FunctionEventBase { + function_type: "parameters"; + function_data: { + type: "parameters"; + data: Record; + __schema: ParametersSchema; + }; +} + class ProjectNameIdMap { private nameToId: Record = {}; private idToName: Record = {}; diff --git a/js/src/functions/invoke.ts b/js/src/functions/invoke.ts index 1cfe9a5b3..ac6e3f1a4 100644 --- a/js/src/functions/invoke.ts +++ b/js/src/functions/invoke.ts @@ -1,10 +1,10 @@ -import { FunctionId as functionIdSchema } from "../generated_types"; +import { FunctionId as functionIdSchema } from "../sdk-schemas"; import type { - InvokeFunctionType as InvokeFunctionRequest, - ChatCompletionMessageParamType as Message, - StreamingModeType as StreamingMode, - FunctionTypeEnumType as FunctionType, -} from "../generated_plain_types"; + InvokeFunction as InvokeFunctionRequest, + ChatCompletionMessageParam as Message, + StreamingMode, + FunctionTypeEnum as FunctionType, +} from "../sdk-types"; import { _internalGetGlobalState, BraintrustState, diff --git a/js/src/functions/stream.ts b/js/src/functions/stream.ts index 901891119..ae0ba64e1 100644 --- a/js/src/functions/stream.ts +++ b/js/src/functions/stream.ts @@ -2,12 +2,12 @@ import { CallEvent as callEventSchema, SSEConsoleEventData as sseConsoleEventDataSchema, SSEProgressEventData as sseProgressEventDataSchema, -} from "../generated_types"; +} from "../sdk-schemas"; import type { - CallEventType as CallEvent, - SSEConsoleEventDataType, - SSEProgressEventDataType, -} from "../generated_plain_types"; + CallEvent, + SSEConsoleEventData, + SSEProgressEventData, +} from "../sdk-types"; import { createParser, EventSourceParser, @@ -25,8 +25,8 @@ export type BraintrustStreamChunk = | { type: "reasoning_delta"; data: string } | { type: "json_delta"; data: string } | { type: "error"; data: string } - | { type: "console"; data: SSEConsoleEventDataType } - | { type: "progress"; data: SSEProgressEventDataType } + | { type: "console"; data: SSEConsoleEventData } + | { type: "progress"; data: SSEProgressEventData } | { type: "start"; data: string } | { type: "done"; data: string }; diff --git a/js/src/gitutil.ts b/js/src/gitutil.ts index 9a687ebb3..091404ce6 100644 --- a/js/src/gitutil.ts +++ b/js/src/gitutil.ts @@ -1,7 +1,4 @@ -import type { - GitMetadataSettingsType as GitMetadataSettings, - RepoInfoType as RepoInfo, -} from "./generated_plain_types"; +import type { GitMetadataSettings, RepoInfo } from "./sdk-types"; import { debugLogger } from "./debug-logger"; import { runGitCommand } from "./git-command"; diff --git a/js/src/isomorph.ts b/js/src/isomorph.ts index c02657d31..c06635d0d 100644 --- a/js/src/isomorph.ts +++ b/js/src/isomorph.ts @@ -1,7 +1,4 @@ -import type { - GitMetadataSettingsType as GitMetadataSettings, - RepoInfoType as RepoInfo, -} from "./generated_plain_types"; +import type { GitMetadataSettings, RepoInfo } from "./sdk-types"; import { newGlobalTracingChannel, type GlobalHookAsyncLocalStorage, diff --git a/js/src/logger.ts b/js/src/logger.ts index 13d85fbf6..b7bd554c0 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -76,27 +76,25 @@ import { DatasetSnapshot as datasetSnapshotSchema, PromptData as promptDataSchema, Prompt as promptSchema, -} from "./generated_types"; +} from "./sdk-schemas"; import type { - AnyModelParamsType as AnyModelParam, - AttachmentReferenceType as AttachmentReference, - BraintrustAttachmentReferenceType as BraintrustAttachmentReference, - ChatCompletionToolType as ChatCompletionTool, - ExternalAttachmentReferenceType as ExternalAttachmentReference, - ModelParamsType as ModelParams, - AttachmentStatusType as AttachmentStatus, - GitMetadataSettingsType as GitMetadataSettings, - ChatCompletionMessageParamType as Message, - ChatCompletionOpenAIMessageParamType as OpenAIMessage, - DatasetSnapshotType as DatasetSnapshot, - PromptDataType as PromptData, - PromptType as PromptRow, - PromptSessionEventType as PromptSessionEvent, - RepoInfoType as RepoInfo, - ObjectReferenceType as ObjectReference, - PromptBlockDataType as PromptBlockData, - ResponseFormatJsonSchemaType as ResponseFormatJsonSchema, -} from "./generated_plain_types"; + AttachmentReference, + BraintrustAttachmentReference, + ChatCompletionTool, + ExternalAttachmentReference, + ModelParams, + AttachmentStatus, + GitMetadataSettings, + ChatCompletionMessageParam as Message, + ChatCompletionOpenAIMessageParam as OpenAIMessage, + DatasetSnapshot, + PromptData, + FunctionTypeEnum, + PromptSessionEvent, + RepoInfo, + ObjectReference, + PromptBlockData, +} from "./sdk-types"; const BRAINTRUST_ATTACHMENT = BraintrustAttachmentReferenceSchema.shape.type.value; @@ -8797,37 +8795,52 @@ export class Dataset extends ObjectFetcher { } type CompiledPromptResponseFormat = - Exclude extends infer ResponseFormat - ? ResponseFormat extends { - type: "json_schema"; - json_schema: infer JsonSchema; - } - ? Omit & { - json_schema: Omit< - Extract, - "schema" - > & { - schema?: Record; - }; - } - : ResponseFormat - : never; - -// "none" is not assignable to older openai clients so we gotta exclude it -type CompiledPromptReasoningEffort = Exclude< - AnyModelParam["reasoning_effort"], - "none" ->; - -export type CompiledPromptParams = Omit< - NonNullable["params"], - "use_cache" | "response_format" | "reasoning_effort" -> & - Omit & { - reasoning_effort?: CompiledPromptReasoningEffort; - response_format?: CompiledPromptResponseFormat; - model: NonNullable["model"]>; - }; + | { type: "json_object" } + | { type: "text" } + | { + type: "json_schema"; + json_schema: { + name: string; + description?: string; + schema?: Record; + strict?: boolean | null; + }; + }; + +// Keep the compiled API independent of the backend's provider parameter union. +// "none" is excluded from reasoning_effort for compatibility with older OpenAI clients. +export interface CompiledPromptParams { + model: string; + max_tokens: number; + temperature?: number; + top_p?: number; + max_completion_tokens?: number; + frequency_penalty?: number; + presence_penalty?: number; + response_format?: CompiledPromptResponseFormat; + tool_choice?: + | "auto" + | "none" + | "required" + | { + type: "function"; + function: { name: string }; + }; + function_call?: "auto" | "none" | { name: string }; + n?: number; + stop?: string[]; + reasoning_effort?: "minimal" | "low" | "medium" | "high"; + verbosity?: "low" | "medium" | "high"; + top_k?: number; + stop_sequences?: string[]; + reasoning_enabled?: boolean; + reasoning_budget?: number; + /** Legacy parameter; prefer max_tokens. */ + max_tokens_to_sample?: number; + maxOutputTokens?: number; + topP?: number; + topK?: number; +} export type ChatPrompt = { messages: OpenAIMessage[]; @@ -8858,9 +8871,13 @@ export type CompiledPrompt = : // eslint-disable-next-line @typescript-eslint/no-empty-object-type {}); -export type DefaultPromptArgs = Partial< - CompiledPromptParams & AnyModelParam & ChatPrompt & CompletionPrompt ->; +export interface DefaultPromptArgs + extends + Partial, + Partial, + Partial { + use_cache?: boolean; +} function isAttachmentObject(value: unknown): boolean { return ( @@ -9024,17 +9041,26 @@ export function renderMessageImpl( }; } +interface PromptMetadata { + name: string; + slug: string; + project_id?: string; + description?: string | null; + created?: string | null; + prompt_data?: PromptData | null; + tags?: string[] | null; + // Preserve the generated prompt row's permissive metadata contract. + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + metadata?: {} | null; + function_type?: FunctionTypeEnum | null; +} + export type PromptRowWithId< HasId extends boolean = true, HasVersion extends boolean = true, -> = Omit & - Partial> & - (HasId extends true - ? Pick - : Partial>) & - (HasVersion extends true - ? Pick - : Partial>); +> = PromptMetadata & + (HasId extends true ? { id: string } : { id?: string }) & + (HasVersion extends true ? { _xact_id: string } : { _xact_id?: string }); export function deserializePlainStringAsJSON(s: string) { if (s.trim() === "") { @@ -9121,6 +9147,16 @@ export function renderPromptParams( return params; } +export interface SerializedPrompt { + metadata: PromptRowWithId | PromptSessionEvent; + defaults: DefaultPromptArgs; + noTrace: boolean; +} + +// Initialized by the class so cache serialization can access private fields +// without exposing a serialization method on the public Prompt API. +export let serializePromptForCache: (prompt: Prompt) => SerializedPrompt; + export class Prompt< HasId extends boolean = true, HasVersion extends boolean = true, @@ -9457,13 +9493,12 @@ export class Prompt< ); } - /** @internal */ - public _internalSerializeForCache() { - return { - metadata: this.metadata, - defaults: this.defaults, - noTrace: this.noTrace, - }; + static { + serializePromptForCache = (prompt) => ({ + metadata: prompt.metadata, + defaults: prompt.defaults, + noTrace: prompt.noTrace, + }); } public static fromPromptData( @@ -9482,6 +9517,14 @@ export class Prompt< } } +export interface SerializedParameters { + metadata: ParametersRow; +} + +export let serializeParametersForCache: ( + parameters: RemoteEvalParameters, +) => SerializedParameters; + export class RemoteEvalParameters< HasId extends boolean = true, HasVersion extends boolean = true, @@ -9528,9 +9571,10 @@ export class RemoteEvalParameters< return (this.metadata.function_data.data ?? {}) as T; } - /** @internal */ - public _internalSerializeForCache() { - return { metadata: this.metadata }; + static { + serializeParametersForCache = (parameters) => ({ + metadata: parameters.metadata, + }); } public validate(data: unknown): boolean { diff --git a/js/src/prompt-cache/parameters-cache.ts b/js/src/prompt-cache/parameters-cache.ts index bb81a7a9c..530cfb28a 100644 --- a/js/src/prompt-cache/parameters-cache.ts +++ b/js/src/prompt-cache/parameters-cache.ts @@ -1,4 +1,8 @@ -import { RemoteEvalParameters } from "../logger"; +import { + RemoteEvalParameters, + serializeParametersForCache, + type SerializedParameters, +} from "../logger"; import { LRUCache } from "../lru-cache"; import { DiskCache } from "./disk-cache"; @@ -35,7 +39,7 @@ export type ParametersMemoryCacheEntry = { }; export type ParametersDiskCacheEntry = { - value: ReturnType; + value: SerializedParameters; resolvedOrgIdentity?: string; }; @@ -111,7 +115,7 @@ export class ParametersCache { this.memoryCache?.set(cacheKey, memoryEntry); if (this.diskCache) { await this.diskCache.set(cacheKey, { - value: value._internalSerializeForCache(), + value: serializeParametersForCache(value), resolvedOrgIdentity: this.expectedResolvedOrgIdentity, }); } diff --git a/js/src/prompt-cache/prompt-cache.ts b/js/src/prompt-cache/prompt-cache.ts index eb58ca0c2..8fa7b291a 100644 --- a/js/src/prompt-cache/prompt-cache.ts +++ b/js/src/prompt-cache/prompt-cache.ts @@ -1,4 +1,8 @@ -import { Prompt } from "../logger"; +import { + Prompt, + serializePromptForCache, + type SerializedPrompt, +} from "../logger"; import { LRUCache } from "../lru-cache"; import { DiskCache } from "./disk-cache"; @@ -40,7 +44,7 @@ export type PromptMemoryCacheEntry = { }; export type PromptDiskCacheEntry = { - value: ReturnType; + value: SerializedPrompt; resolvedOrgIdentity?: string; }; @@ -168,7 +172,7 @@ export class PromptCache { this.memoryCache?.set(cacheKey, memoryEntry); if (this.diskCache) { await this.diskCache.set(cacheKey, { - value: value._internalSerializeForCache(), + value: serializePromptForCache(value), resolvedOrgIdentity: this.expectedResolvedOrgIdentity, }); } diff --git a/js/src/prompt-schemas.ts b/js/src/prompt-schemas.ts index c83c420f0..94a433212 100644 --- a/js/src/prompt-schemas.ts +++ b/js/src/prompt-schemas.ts @@ -1,19 +1,19 @@ import type { - ToolFunctionDefinitionType as ToolFunctionDefinition, - ChatCompletionMessageParamType, - ModelParamsType, - PromptBlockDataType as PromptBlockData, - PromptDataType as PromptData, -} from "./generated_plain_types"; + ToolFunctionDefinition, + ChatCompletionMessageParam, + ModelParams, + PromptBlockData, + PromptData, +} from "./sdk-types"; // This roughly maps to promptBlockDataSchema, but is more ergonomic for the user. export type PromptContents = | { prompt: string } - | { messages: ChatCompletionMessageParamType[] }; + | { messages: ChatCompletionMessageParam[] }; export type PromptDefinition = PromptContents & { model: string; - params?: ModelParamsType; + params?: ModelParams; templateFormat?: "mustache" | "nunjucks" | "none"; environments?: string[]; }; diff --git a/js/src/prompt-types.test.ts b/js/src/prompt-types.test.ts new file mode 100644 index 000000000..51df00f1b --- /dev/null +++ b/js/src/prompt-types.test.ts @@ -0,0 +1,135 @@ +import { expectTypeOf, test } from "vitest"; +import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions"; +import type { CompletionCreateParamsNonStreaming } from "openai/resources/completions"; +import { + Prompt, + type CompiledPrompt, + type CompiledPromptParams, + type DefaultPromptArgs, + type PromptRowWithId, +} from "./logger"; +import type { CodeParameters, CodePrompt } from "./framework2"; +import type { + AnyModelParamsType, + FunctionDataType, + PromptDataType, + PromptType, +} from "./generated_plain_types"; + +test("compiled prompts preserve provider parameter types and compatibility", () => { + // The generated backend types are a compatibility reference, not the public API. + type BackendParams = Omit< + AnyModelParamsType, + "use_cache" | "response_format" | "reasoning_effort" + > & { model: string }; + type Params = Omit< + CompiledPromptParams, + "response_format" | "reasoning_effort" + >; + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf< + CompiledPrompt<"chat"> + >().toMatchTypeOf(); + expectTypeOf< + CompiledPrompt<"completion"> + >().toMatchTypeOf(); + expectTypeOf().toEqualTypeOf< + "minimal" | "low" | "medium" | "high" | undefined + >(); + expectTypeOf().toEqualTypeOf< + boolean | undefined + >(); + + if (false) { + const prompt = null as unknown as Prompt; + // @ts-expect-error Cache serialization is internal, not a Prompt method. + prompt._internalSerializeForCache(); + expectTypeOf(prompt.build({})).toEqualTypeOf>(); + expectTypeOf(prompt.build({}, { flavor: "completion" })).toEqualTypeOf< + CompiledPrompt<"completion"> + >(); + expectTypeOf(prompt.buildWithAttachments({})).toEqualTypeOf< + Promise> + >(); + const defaults: DefaultPromptArgs = { + response_format: { + type: "json_schema", + json_schema: { + name: "result", + schema: { type: "object" }, + strict: null, + }, + }, + use_cache: true, + }; + new Prompt( + { id: "id", _xact_id: "version", name: "name", slug: "slug" }, + defaults, + false, + ); + defaults.response_format = { + type: "json_schema", + json_schema: { + name: "result", + // @ts-expect-error Compiled schemas are objects, not templates. + schema: "{{schema}}", + }, + }; + // @ts-expect-error Preserve compatibility with older OpenAI clients. + defaults.reasoning_effort = "none"; + } +}); + +test("prompt metadata preserves required and optional identifiers", () => { + type Metadata = Omit< + PromptType, + "log_id" | "org_id" | "project_id" | "id" | "_xact_id" + > & { + project_id?: string; + }; + type RequiredRow = Metadata & { id: string; _xact_id: string }; + type OptionalRow = Metadata & { id?: string; _xact_id?: string }; + expectTypeOf>().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf>(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf>().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf>(); + expectTypeOf["id"]>().toEqualTypeOf(); + expectTypeOf["_xact_id"]>().toEqualTypeOf< + string | undefined + >(); + expectTypeOf["id"]>().toEqualTypeOf< + string | undefined + >(); + expectTypeOf< + PromptRowWithId["_xact_id"] + >().toEqualTypeOf(); + expectTypeOf["id"]>().toEqualTypeOf(); + expectTypeOf["id"]>().toEqualTypeOf< + string | undefined + >(); +}); + +test("function definitions expose only the variant each builder produces", () => { + type PromptEvent = Awaited>; + type ParametersEvent = Awaited< + ReturnType + >; + expectTypeOf().toEqualTypeOf<{ + type: "prompt"; + }>(); + expectTypeOf< + PromptEvent["prompt_data"] + >().branded.toEqualTypeOf(); + expectTypeOf< + ParametersEvent["function_data"]["type"] + >().toEqualTypeOf<"parameters">(); + expectTypeOf< + ParametersEvent["function_type"] + >().toEqualTypeOf<"parameters">(); + expectTypeOf().toMatchTypeOf< + Extract + >(); +}); diff --git a/js/src/public-types.test.ts b/js/src/public-types.test.ts index 3a37a9cf7..2870520fd 100644 --- a/js/src/public-types.test.ts +++ b/js/src/public-types.test.ts @@ -53,6 +53,8 @@ test("does not expose runtime schemas or implementation helpers", () => { "SpanImpl", "IDGenerator", "_exportsForTestingOnly", + "serializePromptForCache", + "serializeParametersForCache", "default", ]) { expect(publicExports).not.toHaveProperty(name); diff --git a/js/src/sandbox.ts b/js/src/sandbox.ts index 58c9f45be..4f91fc753 100644 --- a/js/src/sandbox.ts +++ b/js/src/sandbox.ts @@ -1,6 +1,6 @@ import { z } from "zod/v3"; import { slugify } from "../util/string_util"; -import type { IfExistsType } from "./generated_plain_types"; +import type { IfExists } from "./sdk-types"; import { type BraintrustState, _internalGetGlobalState } from "./logger"; /** @@ -32,7 +32,7 @@ interface RegisterSandboxOptions { /** Optional metadata. */ metadata?: Record; /** What to do if function already exists. Defaults to "replace". */ - ifExists?: IfExistsType; + ifExists?: IfExists; /** Braintrust API key. Uses BRAINTRUST_API_KEY env var if not provided. */ apiKey?: string; /** Braintrust app URL. Uses default if not provided. */ diff --git a/js/src/sdk-contracts.test.ts b/js/src/sdk-contracts.test.ts new file mode 100644 index 000000000..00ec7cd07 --- /dev/null +++ b/js/src/sdk-contracts.test.ts @@ -0,0 +1,180 @@ +import { expectTypeOf, test } from "vitest"; +import type { z } from "zod/v3"; +import type * as Backend from "./generated_plain_types"; +import type * as backendSchemas from "./generated_types"; +import type * as SDK from "./sdk-types"; +import type * as schemas from "./sdk-schemas"; + +// Backend definitions are compatibility fixtures, never production dependencies. +test("SDK-owned contracts remain compatible with the backend", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf< + Exclude + >().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().branded.toEqualTypeOf(); + expectTypeOf< + Exclude + >().toEqualTypeOf(); + expectTypeOf().branded.toEqualTypeOf(); + // Structural equality accommodates explicit fields replacing mapped intersections. + expectTypeOf().branded.toEqualTypeOf(); + expectTypeOf().toExtend(); + expectTypeOf().toExtend(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().branded.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().branded.toEqualTypeOf(); + expectTypeOf().branded.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); +}); + +test("runtime validators accept and return compatible types", () => { + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf< + z.input + >().toEqualTypeOf< + z.input + >(); + expectTypeOf< + z.output + >().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf< + z.input + >().toEqualTypeOf< + z.input + >(); + expectTypeOf< + z.output + >().toEqualTypeOf< + z.output + >(); + expectTypeOf< + z.input + >().toEqualTypeOf>(); + expectTypeOf< + z.output + >().toEqualTypeOf>(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); + expectTypeOf>().toEqualTypeOf< + z.input + >(); + expectTypeOf>().toEqualTypeOf< + z.output + >(); +}); diff --git a/js/src/sdk-schemas.ts b/js/src/sdk-schemas.ts new file mode 100644 index 000000000..70ae00ae1 --- /dev/null +++ b/js/src/sdk-schemas.ts @@ -0,0 +1,573 @@ +/** + * Runtime validators for the SDK-owned subset of the Braintrust API. + * Input/output type compatibility is checked in sdk-contracts.test.ts. + */ +import { z } from "zod/v3"; + +export const ResponseFormatJsonSchema = z.object({ + name: z.string(), + description: z.string().optional(), + schema: z + .union([z.object({}).partial().passthrough(), z.string()]) + .optional(), + strict: z.union([z.boolean(), z.null()]).optional(), +}); + +const ResponseFormatNullish = z.union([ + z.object({ type: z.literal("json_object") }), + z.object({ + type: z.literal("json_schema"), + json_schema: ResponseFormatJsonSchema, + }), + z.object({ type: z.literal("text") }), + z.null(), +]); + +export const BraintrustAttachmentReference = z.object({ + type: z.literal("braintrust_attachment"), + filename: z.string().min(1), + content_type: z.string().min(1), + key: z.string().min(1), +}); + +export const ExternalAttachmentReference = z.object({ + type: z.literal("external_attachment"), + filename: z.string().min(1), + content_type: z.string().min(1), + url: z.string().min(1), +}); + +export const AttachmentReference = z.discriminatedUnion("type", [ + BraintrustAttachmentReference, + ExternalAttachmentReference, +]); + +const UploadStatus = z.enum(["uploading", "done", "error"]); + +export const AttachmentStatus = z.object({ + upload_status: UploadStatus, + error_message: z.string().optional(), +}); + +const FunctionTypeEnum = z.enum([ + "llm", + "scorer", + "task", + "tool", + "custom_view", + "preprocessor", + "facet", + "classifier", + "tag", + "parameters", + "sandbox", +]); + +const SavedFunctionId = z.union([ + z.object({ + type: z.literal("function"), + id: z.string(), + version: z.string().optional(), + }), + z.object({ + type: z.literal("global"), + name: z.string(), + function_type: FunctionTypeEnum.optional().default("scorer"), + }), +]); + +export const BraintrustModelParams = z + .object({ + use_cache: z.boolean(), + reasoning_enabled: z.boolean(), + reasoning_budget: z.number(), + }) + .partial(); + +export const CallEvent = z.union([ + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("text_delta"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("reasoning_delta"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("json_delta"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("progress"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("error"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("console"), + }), + z.object({ + id: z.string().optional(), + event: z.literal("start"), + data: z.literal(""), + }), + z.object({ + id: z.string().optional(), + event: z.literal("done"), + data: z.literal(""), + }), +]); + +const CacheControl = z.object({ + type: z.literal("ephemeral"), + ttl: z.enum(["5m", "1h"]).optional(), +}); + +const ChatCompletionContentPartText = z.object({ + text: z.string().default(""), + type: z.literal("text"), + cache_control: CacheControl.optional(), +}); + +const ChatCompletionContentPartImageWithTitle = z.object({ + image_url: z.object({ + url: z.string(), + detail: z + .union([z.literal("auto"), z.literal("low"), z.literal("high")]) + .optional(), + }), + type: z.literal("image_url"), + cache_control: CacheControl.optional(), +}); + +const ChatCompletionContentPartFileFile = z + .object({ file_data: z.string(), filename: z.string(), file_id: z.string() }) + .partial(); + +const ChatCompletionContentPartFileWithTitle = z.object({ + file: ChatCompletionContentPartFileFile, + type: z.literal("file"), + cache_control: CacheControl.optional(), +}); + +const ChatCompletionContentPart = z.union([ + ChatCompletionContentPartText, + ChatCompletionContentPartImageWithTitle, + ChatCompletionContentPartFileWithTitle, +]); + +const ChatCompletionMessageToolCall = z.object({ + id: z.string(), + function: z.object({ arguments: z.string(), name: z.string() }), + type: z.literal("function"), +}); + +const ChatCompletionMessageReasoning = z + .object({ id: z.string(), content: z.string() }) + .partial(); + +const ChatCompletionMessageParam = z.union([ + z.object({ + content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), + role: z.literal("system"), + name: z.string().optional(), + }), + z.object({ + content: z.union([z.string(), z.array(ChatCompletionContentPart)]), + role: z.literal("user"), + name: z.string().optional(), + }), + z.object({ + role: z.literal("assistant"), + content: z + .union([z.string(), z.array(ChatCompletionContentPartText), z.null()]) + .optional(), + function_call: z + .object({ arguments: z.string(), name: z.string() }) + .optional(), + name: z.string().optional(), + tool_calls: z.array(ChatCompletionMessageToolCall).optional(), + reasoning: z.array(ChatCompletionMessageReasoning).optional(), + reasoning_signature: z.string().optional(), + }), + z.object({ + content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), + role: z.literal("tool"), + tool_call_id: z.string().default(""), + }), + z.object({ + content: z.union([z.string(), z.null()]), + name: z.string(), + role: z.literal("function"), + }), + z.object({ + content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), + role: z.literal("developer"), + name: z.string().optional(), + }), + z.object({ + role: z.literal("model"), + content: z.union([z.string(), z.null()]).optional(), + }), +]); + +export const ChatCompletionTool = z.object({ + function: z.object({ + name: z.string(), + description: z.string().optional(), + parameters: z.object({}).partial().passthrough().optional(), + }), + type: z.literal("function"), +}); + +export const ObjectReference = z.object({ + object_type: z.enum([ + "project_logs", + "experiment", + "dataset", + "prompt", + "function", + "prompt_session", + ]), + object_id: z.string().uuid(), + id: z.string(), + _xact_id: z.union([z.string(), z.null()]).optional(), + created: z.union([z.string(), z.null()]).optional(), +}); + +export const DatasetSnapshot = z.object({ + id: z.string().uuid(), + dataset_id: z.string().uuid(), + name: z.string(), + description: z.union([z.string(), z.null()]), + xact_id: z.string(), + created: z.union([z.string(), z.null()]), +}); + +const PromptBlockDataNullish = z.union([ + z.object({ + type: z.literal("chat"), + messages: z.array(ChatCompletionMessageParam), + tools: z.string().optional(), + }), + z.object({ type: z.literal("completion"), content: z.string() }), + z.null(), +]); + +const ModelParams = z.union([ + BraintrustModelParams.extend({ + temperature: z.number(), + top_p: z.number(), + max_tokens: z.number(), + max_completion_tokens: z.number(), + frequency_penalty: z.number(), + presence_penalty: z.number(), + response_format: ResponseFormatNullish, + tool_choice: z.union([ + z.literal("auto"), + z.literal("none"), + z.literal("required"), + z.object({ + type: z.literal("function"), + function: z.object({ name: z.string() }), + }), + ]), + function_call: z.union([ + z.literal("auto"), + z.literal("none"), + z.object({ name: z.string() }), + ]), + n: z.number(), + stop: z.array(z.string()), + reasoning_effort: z.enum(["none", "minimal", "low", "medium", "high"]), + verbosity: z.enum(["low", "medium", "high"]), + }) + .partial() + .passthrough(), + BraintrustModelParams.extend({ + max_tokens: z.number(), + temperature: z.number(), + top_p: z.number().optional(), + top_k: z.number().optional(), + stop_sequences: z.array(z.string()).optional(), + max_tokens_to_sample: z.number().optional(), + }).passthrough(), + BraintrustModelParams.extend({ + temperature: z.number(), + maxOutputTokens: z.number(), + topP: z.number(), + topK: z.number(), + }) + .partial() + .passthrough(), + BraintrustModelParams.extend({ + temperature: z.number(), + topK: z.number(), + }) + .partial() + .passthrough(), + BraintrustModelParams.passthrough(), +]); + +const PromptOptionsNullish = z.union([ + z + .object({ + model: z.string(), + params: ModelParams, + position: z.string(), + endpoint_name: z.union([z.string(), z.null()]), + }) + .partial(), + z.null(), +]); + +const PromptParserNullish = z.union([ + z.object({ + type: z.literal("llm_classifier"), + use_cot: z.boolean(), + choice_scores: z.record(z.number().gte(0).lte(1)).optional(), + choice: z.array(z.string()).optional(), + allow_no_match: z.boolean().optional(), + allow_skip: z.boolean().optional(), + }), + z.null(), +]); + +const PreprocessorId = z.union([ + z.object({ + type: z.literal("function"), + id: z.string(), + version: z.string().optional(), + }), + z.object({ + type: z.literal("global"), + name: z.string(), + function_type: z.literal("preprocessor").optional().default("preprocessor"), + }), + z.object({ type: z.literal("inline"), code: z.string().min(1) }), + z.null(), +]); + +const FunctionFormat = z.enum(["llm", "code", "global", "graph", "topic_map"]); + +export const PromptData = z + .object({ + prompt: PromptBlockDataNullish, + options: PromptOptionsNullish, + parser: PromptParserNullish, + preprocessor: PreprocessorId, + tool_functions: z.union([z.array(SavedFunctionId), z.null()]), + template_format: z.union([ + z.enum(["mustache", "nunjucks", "none"]), + z.null(), + ]), + mcp: z.union([ + z.record( + z.union([ + z.object({ + type: z.literal("id"), + id: z.string().uuid(), + is_disabled: z.boolean().optional(), + enabled_tools: z.union([z.array(z.string()), z.null()]).optional(), + }), + z.object({ + type: z.literal("url"), + url: z.string(), + is_disabled: z.boolean().optional(), + enabled_tools: z.union([z.array(z.string()), z.null()]).optional(), + }), + ]), + ), + z.null(), + ]), + origin: z.union([ + z + .object({ + prompt_id: z.string(), + project_id: z.string(), + prompt_version: z.string(), + }) + .partial(), + z.null(), + ]), + }) + .partial(); + +export const FunctionId = z.union([ + z.object({ function_id: z.string(), version: z.string().optional() }), + z.object({ + project_name: z.string(), + slug: z.string(), + version: z.string().optional(), + }), + z.object({ + global_function: z.string(), + function_type: FunctionTypeEnum.optional().default("scorer"), + }), + z.object({ + prompt_session_id: z.string(), + prompt_session_function_id: z.string(), + version: z.string().optional(), + }), + z.object({ + inline_context: z.object({ + runtime: z.enum(["node", "python", "browser", "quickjs"]), + version: z.string(), + }), + code: z.string(), + function_type: FunctionTypeEnum.and(z.unknown()).optional(), + name: z.union([z.string(), z.null()]).optional(), + }), + z.object({ + inline_prompt: PromptData.optional(), + inline_function: z.object({}).partial().passthrough(), + function_type: FunctionTypeEnum.optional().default("scorer"), + name: z.union([z.string(), z.null()]).optional(), + }), + z.object({ + inline_prompt: PromptData, + function_type: FunctionTypeEnum.optional().default("scorer"), + name: z.union([z.string(), z.null()]).optional(), + }), +]); + +const FunctionObjectType = z.enum([ + "prompt", + "tool", + "scorer", + "task", + "workflow", + "custom_view", + "preprocessor", + "facet", + "classifier", + "parameters", + "sandbox", +]); + +const FunctionOutputType = z.enum([ + "completion", + "score", + "facet", + "classification", + "any", +]); + +export const GitMetadataSettings = z.object({ + collect: z.enum(["all", "none", "some"]), + fields: z + .array( + z.enum([ + "commit", + "branch", + "tag", + "dirty", + "author_name", + "author_email", + "commit_message", + "commit_time", + "git_diff", + ]), + ) + .optional(), +}); + +const ProjectSettings = z.union([ + z + .object({ + comparison_key: z.union([z.string(), z.null()]), + baseline_experiment_id: z.union([z.string(), z.null()]), + spanFieldOrder: z.union([ + z.array( + z.object({ + object_type: z.string(), + column_id: z.string(), + position: z.string(), + layout: z + .union([z.literal("full"), z.literal("two_column"), z.null()]) + .optional(), + }), + ), + z.null(), + ]), + remote_eval_sources: z.union([ + z.array( + z.object({ + url: z.string(), + name: z.union([z.string(), z.null()]).optional(), + description: z.union([z.string(), z.null()]).optional(), + }), + ), + z.null(), + ]), + disable_realtime_queries: z.union([z.boolean(), z.null()]), + monitor_charts_use_metrics_start: z.union([z.boolean(), z.null()]), + blind_reviews: z.union([z.boolean(), z.null()]), + default_preprocessor: z.union([...SavedFunctionId.options, z.null()]), + }) + .partial(), + z.null(), +]); + +export const Project = z.object({ + id: z.string().uuid(), + org_id: z.string().uuid(), + name: z.string(), + description: z.union([z.string(), z.null()]).optional(), + created: z.union([z.string(), z.null()]).optional(), + deleted_at: z.union([z.string(), z.null()]).optional(), + user_id: z.union([z.string(), z.null()]).optional(), + settings: ProjectSettings.optional(), +}); + +export const Prompt = z.object({ + id: z.string().uuid(), + _xact_id: z.string(), + project_id: z.string().uuid(), + log_id: z.literal("p"), + org_id: z.string().uuid(), + name: z.string(), + slug: z.string(), + description: z.union([z.string(), z.null()]).optional(), + created: z.union([z.string(), z.null()]).optional(), + prompt_data: z.union([PromptData, z.null()]).optional(), + tags: z.union([z.array(z.string()), z.null()]).optional(), + metadata: z + .union([z.object({}).partial().passthrough(), z.null()]) + .optional(), + function_type: z.union([FunctionTypeEnum, z.null()]).optional(), +}); + +export const SSEConsoleEventData = z.object({ + stream: z.enum(["stderr", "stdout"]), + message: z.string(), +}); + +export const SSEProgressEventData = z.object({ + id: z.string(), + object_type: FunctionObjectType, + origin: z.union([ObjectReference, z.null()]).and(z.unknown()).optional(), + format: FunctionFormat, + output_type: FunctionOutputType, + name: z.string(), + event: z.enum([ + "reasoning_delta", + "text_delta", + "json_delta", + "error", + "console", + "start", + "done", + "progress", + ]), + data: z.string(), +}); diff --git a/js/src/sdk-types.ts b/js/src/sdk-types.ts new file mode 100644 index 000000000..1fdc3f6a9 --- /dev/null +++ b/js/src/sdk-types.ts @@ -0,0 +1,573 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type -- Preserve the existing wire contracts, including permissive JSON fields. */ +/* eslint-disable @typescript-eslint/no-explicit-any -- Provider parameter maps intentionally accept arbitrary values. */ +/** + * SDK-owned wire contracts. Keep backend-generated definitions in compatibility + * tests only; changes here should describe the fields the SDK actually uses. + */ +export interface ResponseFormatJsonSchema { + name: string; + description?: string | undefined; + schema?: ({} | string) | undefined; + strict?: (boolean | null) | undefined; +} +export type ResponseFormatNullish = + | { + type: "json_object"; + } + | { + type: "json_schema"; + json_schema: ResponseFormatJsonSchema; + } + | { + type: "text"; + } + | null; +export type AsyncScoringState = + | { + status: "enabled"; + token: string; + function_ids: Array; + skip_logging?: (boolean | null) | undefined; + triggered_functions?: ({} | null) | undefined; + last_triggered_xact_id?: (string | number | null) | undefined; + } + | { + status: "disabled"; + } + | null; +export type AsyncScoringControl = + | { + kind: "score_update"; + token?: string | undefined; + } + | { + kind: "state_override"; + state: AsyncScoringState; + } + | { + kind: "state_force_reselect"; + } + | { + kind: "state_enabled_force_rescore"; + } + | { + kind: "trigger_functions"; + triggered_functions: Array<{ + function_id?: unknown | undefined; + scope: + | { + type: "span"; + } + | { + type: "trace"; + }; + idempotency_key?: string | undefined; + }>; + } + | { + kind: "complete_triggered_functions"; + function_ids: Array; + triggered_xact_id: string; + } + | { + kind: "mark_attempt_failed"; + function_ids: Array; + }; +export interface BraintrustAttachmentReference { + type: "braintrust_attachment"; + filename: string; + content_type: string; + key: string; +} +export interface ExternalAttachmentReference { + type: "external_attachment"; + filename: string; + content_type: string; + url: string; +} +export type AttachmentReference = + | BraintrustAttachmentReference + | ExternalAttachmentReference; +export type UploadStatus = "uploading" | "done" | "error"; +export interface AttachmentStatus { + upload_status: UploadStatus; + error_message?: string | undefined; +} +export type FunctionTypeEnum = + | "llm" + | "scorer" + | "task" + | "tool" + | "custom_view" + | "preprocessor" + | "facet" + | "classifier" + | "tag" + | "parameters" + | "sandbox"; +export type SavedFunctionId = + | { + type: "function"; + id: string; + version?: string | undefined; + } + | { + type: "global"; + name: string; + function_type: FunctionTypeEnum; + }; +export type CallEvent = + | { + id?: string | undefined; + data: string; + event: "text_delta"; + } + | { + id?: string | undefined; + data: string; + event: "reasoning_delta"; + } + | { + id?: string | undefined; + data: string; + event: "json_delta"; + } + | { + id?: string | undefined; + data: string; + event: "progress"; + } + | { + id?: string | undefined; + data: string; + event: "error"; + } + | { + id?: string | undefined; + data: string; + event: "console"; + } + | { + id?: string | undefined; + event: "start"; + data: ""; + } + | { + id?: string | undefined; + event: "done"; + data: ""; + }; +interface CacheControl { + type: "ephemeral"; + ttl?: "5m" | "1h" | undefined; +} + +export interface ChatCompletionContentPartImageWithTitle { + image_url: { + url: string; + detail?: ("auto" | "low" | "high") | undefined; + }; + type: "image_url"; + cache_control?: CacheControl | undefined; +} +export interface ChatCompletionContentPartFileFile { + file_data?: string; + filename?: string; + file_id?: string; +} +export interface ChatCompletionContentPartFileWithTitle { + file: ChatCompletionContentPartFileFile; + type: "file"; + cache_control?: CacheControl | undefined; +} +export type ChatCompletionContentPart = + | ChatCompletionContentPartText + | ChatCompletionContentPartImageWithTitle + | ChatCompletionContentPartFileWithTitle; +export interface ChatCompletionContentPartText { + text: string; + type: "text"; + cache_control?: CacheControl | undefined; +} +export interface ChatCompletionMessageToolCall { + id: string; + function: { + arguments: string; + name: string; + }; + type: "function"; +} +export interface ChatCompletionMessageReasoning { + id?: string; + content?: string; +} +export type ChatCompletionMessageParam = + | ChatCompletionOpenAIMessageParam + | { + role: "model"; + content?: string | null | undefined; + }; +export type ChatCompletionOpenAIMessageParam = + | { + content: string | Array; + role: "system"; + name?: string | undefined; + } + | { + content: string | Array; + role: "user"; + name?: string | undefined; + } + | { + role: "assistant"; + content?: + | (string | Array | null) + | undefined; + function_call?: + | { + arguments: string; + name: string; + } + | undefined; + name?: string | undefined; + tool_calls?: Array | undefined; + reasoning?: Array | undefined; + reasoning_signature?: string | undefined; + } + | { + content: string | Array; + role: "tool"; + tool_call_id: string; + } + | { + content: string | null; + name: string; + role: "function"; + } + | { + content: string | Array; + role: "developer"; + name?: string | undefined; + }; +export interface ChatCompletionTool { + function: { + name: string; + description?: string | undefined; + parameters?: {} | undefined; + }; + type: "function"; +} +export interface DatasetSnapshot { + id: string; + dataset_id: string; + name: string; + description: string | null; + xact_id: string; + created: string | null; +} +export type RepoInfo = { + commit?: string | null; + branch?: string | null; + tag?: string | null; + dirty?: boolean | null; + author_name?: string | null; + author_email?: string | null; + commit_message?: string | null; + commit_time?: string | null; + git_diff?: string | null; +} | null; +export type ExtendedSavedFunctionId = + | SavedFunctionId + | { + type: "slug"; + project_id: string; + slug: string; + }; +// Keep provider fields visible to editor completion while accepting custom options. +export type ModelParams = + | { + use_cache?: boolean; + reasoning_enabled?: boolean; + reasoning_budget?: number; + temperature?: number; + top_p?: number; + max_tokens?: number; + max_completion_tokens?: number; + frequency_penalty?: number; + presence_penalty?: number; + response_format?: ResponseFormatNullish; + tool_choice?: + | "auto" + | "none" + | "required" + | { + type: "function"; + function: { + name: string; + }; + }; + function_call?: + | "auto" + | "none" + | { + name: string; + }; + n?: number; + stop?: Array; + reasoning_effort?: "none" | "minimal" | "low" | "medium" | "high"; + verbosity?: "low" | "medium" | "high"; + [key: string]: any; + } + | { + use_cache?: boolean | undefined; + reasoning_enabled?: boolean | undefined; + reasoning_budget?: number | undefined; + max_tokens: number; + temperature: number; + top_p?: number | undefined; + top_k?: number | undefined; + stop_sequences?: Array | undefined; + max_tokens_to_sample?: number | undefined; + [key: string]: any; + } + | { + use_cache?: boolean; + reasoning_enabled?: boolean; + reasoning_budget?: number; + temperature?: number; + maxOutputTokens?: number; + topP?: number; + topK?: number; + [key: string]: any; + } + | { + use_cache?: boolean; + reasoning_enabled?: boolean; + reasoning_budget?: number; + temperature?: number; + topK?: number; + [key: string]: any; + } + | { + use_cache?: boolean; + reasoning_enabled?: boolean; + reasoning_budget?: number; + [key: string]: any; + }; +export type PromptOptionsNullish = { + model?: string; + params?: ModelParams; + position?: string; + endpoint_name?: string | null; +} | null; +export type PromptParserNullish = { + type: "llm_classifier"; + use_cot: boolean; + choice_scores?: {} | undefined; + choice?: Array | undefined; + allow_no_match?: boolean | undefined; + allow_skip?: boolean | undefined; +} | null; +export type PreprocessorId = + | { + type: "function"; + id: string; + version?: string | undefined; + } + | { + type: "global"; + name: string; + function_type: "preprocessor"; + } + | { + type: "inline"; + code: string; + } + | null; +export type PromptBlockData = + | { + type: "chat"; + messages: Array; + tools?: string | undefined; + } + | { + type: "completion"; + content: string; + }; +export type FunctionFormat = "llm" | "code" | "global" | "graph" | "topic_map"; +export interface PromptData { + prompt?: PromptBlockData | null; + options?: PromptOptionsNullish; + parser?: PromptParserNullish; + preprocessor?: PreprocessorId; + tool_functions?: Array | null; + template_format?: ("mustache" | "nunjucks" | "none") | null; + mcp?: {} | null; + origin?: { + prompt_id?: string; + project_id?: string; + prompt_version?: string; + } | null; +} +export type FunctionId = + | { + function_id: string; + version?: string | undefined; + } + | { + project_name: string; + slug: string; + version?: string | undefined; + } + | { + global_function: string; + function_type: FunctionTypeEnum; + } + | { + prompt_session_id: string; + prompt_session_function_id: string; + version?: string | undefined; + } + | { + inline_context: { + runtime: "node" | "python" | "browser" | "quickjs"; + version: string; + }; + code: string; + function_type?: FunctionTypeEnum | undefined; + name?: (string | null) | undefined; + } + | { + inline_prompt?: PromptData | undefined; + inline_function: {}; + function_type: FunctionTypeEnum; + name?: (string | null) | undefined; + } + | { + inline_prompt: PromptData; + function_type: FunctionTypeEnum; + name?: (string | null) | undefined; + }; +export type FunctionObjectType = + | "prompt" + | "tool" + | "scorer" + | "task" + | "workflow" + | "custom_view" + | "preprocessor" + | "facet" + | "classifier" + | "parameters" + | "sandbox"; +export type FunctionOutputType = + | "completion" + | "score" + | "facet" + | "classification" + | "any"; +export interface GitMetadataSettings { + collect: "all" | "none" | "some"; + fields?: + | Array< + | "commit" + | "branch" + | "tag" + | "dirty" + | "author_name" + | "author_email" + | "commit_message" + | "commit_time" + | "git_diff" + > + | undefined; +} +export type IfExists = "error" | "ignore" | "replace"; +export type InvokeParent = + | { + object_type: "project_logs" | "experiment" | "playground_logs"; + object_id: string; + row_ids?: + | ({ + id: string; + span_id: string; + root_span_id: string; + } | null) + | undefined; + propagated_event?: ({} | null) | undefined; + } + | string; +export type StreamingMode = ("auto" | "parallel" | "json" | "text") | null; +export type InvokeFunction = FunctionId & { + input?: unknown; + expected?: unknown; + metadata?: {} | null; + tags?: Array | null; + messages?: Array; + parent?: InvokeParent; + stream?: boolean | null; + mode?: StreamingMode; + strict?: boolean | null; + mcp_auth?: {}; + overrides?: {} | null; + endpoint_name?: string | null; +}; +export interface ObjectReference { + object_type: + | "project_logs" + | "experiment" + | "dataset" + | "prompt" + | "function" + | "prompt_session"; + object_id: string; + id: string; + _xact_id?: (string | null) | undefined; + created?: (string | null) | undefined; +} +export interface PromptSessionEvent { + id: string; + _xact_id: string; + created: string; + _pagination_key?: (string | null) | undefined; + project_id: string; + prompt_session_id: string; + prompt_session_data?: unknown | undefined; + prompt_data?: unknown | undefined; + function_data?: unknown | undefined; + function_type?: FunctionTypeEnum | null | undefined; + object_data?: unknown | undefined; + completion?: unknown | undefined; + tags?: (Array | null) | undefined; +} +export interface SSEConsoleEventData { + stream: "stderr" | "stdout"; + message: string; +} +export interface SSEProgressEventData { + id: string; + object_type: FunctionObjectType; + origin?: (ObjectReference | null) | undefined; + format: FunctionFormat; + output_type: FunctionOutputType; + name: string; + event: + | "reasoning_delta" + | "text_delta" + | "json_delta" + | "error" + | "console" + | "start" + | "done" + | "progress"; + data: string; +} +export interface ToolFunctionDefinition { + type: "function"; + function: { + name: string; + description?: string | undefined; + parameters?: {} | undefined; + strict?: (boolean | null) | undefined; + }; +} diff --git a/js/tests/api-compatibility/api-compatibility.test.ts b/js/tests/api-compatibility/api-compatibility.test.ts index 6241d23d6..0b32fb552 100644 --- a/js/tests/api-compatibility/api-compatibility.test.ts +++ b/js/tests/api-compatibility/api-compatibility.test.ts @@ -2983,6 +2983,22 @@ describe("API Compatibility", () => { } }); + test("keeps prompt implementation types out of declaration chunks", () => { + const dist = path.join(__dirname, "..", "..", "dist"); + const declarations = fs + .readdirSync(dist, { recursive: true }) + .filter((file) => /\.d\.(?:ts|mts)$/.test(String(file))) + .map((file) => fs.readFileSync(path.join(dist, String(file)), "utf8")) + .join("\n"); + + expect(declarations).toContain("interface CompiledPromptParams"); + expect(declarations).not.toContain("_internalSerializeForCache"); + expect(declarations).not.toMatch(/generated_(?:plain_)?types/); + expect(declarations).not.toMatch( + /\b(?:FunctionDataType|AnyModelParamsType|PromptType)\b/, + ); + }); + test("should not regress public API surface for all entrypoints", async () => { if (!publishedVersion) { console.log("Skipping test: No published version available"); diff --git a/js/util/git_fields.ts b/js/util/git_fields.ts index 482a43526..a33b07ffe 100644 --- a/js/util/git_fields.ts +++ b/js/util/git_fields.ts @@ -1,4 +1,4 @@ -import type { GitMetadataSettingsType as GitMetadataSettings } from "./generated_plain_types"; +import type { GitMetadataSettings } from "../src/sdk-types"; export function mergeGitMetadataSettings( s1: GitMetadataSettings, diff --git a/js/util/object.ts b/js/util/object.ts index 074aa6229..e34e014da 100644 --- a/js/util/object.ts +++ b/js/util/object.ts @@ -1,7 +1,4 @@ -import type { - AsyncScoringControlType as AsyncScoringControl, - ObjectReferenceType, -} from "./generated_plain_types"; +import type { AsyncScoringControl, ObjectReference } from "../src/sdk-types"; import { Source, ASYNC_SCORING_CONTROL_FIELD, @@ -23,7 +20,7 @@ export type OtherExperimentLogFields = { classifications?: Record; metadata: Record; metrics: Record; - origin: ObjectReferenceType; + origin: ObjectReference; span_attributes: Record; [ASYNC_SCORING_CONTROL_FIELD]: AsyncScoringControl; [MERGE_PATHS_FIELD]: string[][]; @@ -101,7 +98,7 @@ export type DatasetEvent = { tags?: string[]; metadata?: unknown; created?: string; - origin?: ObjectReferenceType; + origin?: ObjectReference; id: string; dataset_id: string; } & ({ expected?: unknown } | { output?: unknown }); diff --git a/js/util/span_identifier_v4.ts b/js/util/span_identifier_v4.ts index 771d72aec..844730365 100644 --- a/js/util/span_identifier_v4.ts +++ b/js/util/span_identifier_v4.ts @@ -20,7 +20,7 @@ import { uint8ArrayToString, } from "./bytes"; import { z } from "zod/v3"; -import type { InvokeFunctionType as InvokeFunctionRequest } from "./generated_plain_types"; +import type { InvokeFunction as InvokeFunctionRequest } from "../src/sdk-types"; import { mergeDicts } from "./object_util"; const ENCODING_VERSION_NUMBER_V4 = 4; From 5b037de6c341093c840b4aa55cafc977ce7408bb Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 24 Sep 2026 14:09:26 -0400 Subject: [PATCH 13/19] ref!: Remove packaged `bt` binary from `braintrust` package (#2520) - Installing the JavaScript SDK previously also installed the `bt` CLI. Remove its launcher, binary dependencies, and automatic downloader so the SDK installs independently. - Update documentation to explain separate CLI installation and use `bt` directly in examples. - Verify package contents, build, and tracing and evaluation replay tests, including successful delivery to Braintrust. Ref https://linear.app/braintrustdata/issue/SDK-321 --- .changeset/remove-packaged-bt.md | 5 + README.md | 10 +- js/README.md | 3 + js/bin/bt | 41 --------- js/package.json | 16 ---- js/scripts/bt-helper.js | 139 ---------------------------- js/scripts/install.js | 153 ------------------------------- js/src/node/index.ts | 5 +- pnpm-lock.yaml | 81 ---------------- pnpm-workspace.yaml | 2 - 10 files changed, 16 insertions(+), 439 deletions(-) create mode 100644 .changeset/remove-packaged-bt.md delete mode 100755 js/bin/bt delete mode 100644 js/scripts/bt-helper.js delete mode 100644 js/scripts/install.js diff --git a/.changeset/remove-packaged-bt.md b/.changeset/remove-packaged-bt.md new file mode 100644 index 000000000..3f6d26903 --- /dev/null +++ b/.changeset/remove-packaged-bt.md @@ -0,0 +1,5 @@ +--- +"braintrust": major +--- + +ref!: Remove packaged `bt` binary from `braintrust` package diff --git a/README.md b/README.md index a8f2c7cdd..f72e5da85 100644 --- a/README.md +++ b/README.md @@ -34,25 +34,25 @@ Eval("Say Hi Bot", { }); ``` -Run it: +Install the `bt` CLI separately using the standalone installation instructions in the [CLI quickstart](https://www.braintrust.dev/docs/reference/cli/quickstart), then run it: ```bash -BRAINTRUST_API_KEY= npx bt eval tutorial.eval.ts +BRAINTRUST_API_KEY= bt eval tutorial.eval.ts ``` ## Packages | Package | Purpose | npm | Docs | | ---------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| `braintrust` | Core JavaScript/TypeScript SDK for logging, tracing, evals, and CLI. | [![npm: braintrust](https://img.shields.io/npm/v/braintrust.svg)](https://www.npmjs.com/package/braintrust) | [js/README.md](js/README.md) | +| `braintrust` | Core JavaScript/TypeScript SDK for logging, tracing, and evals. | [![npm: braintrust](https://img.shields.io/npm/v/braintrust.svg)](https://www.npmjs.com/package/braintrust) | [js/README.md](js/README.md) | | `@braintrust/browser` | Browser-focused SDK integration with AsyncLocalStorage polyfill support. | [![npm: @braintrust/browser](https://img.shields.io/npm/v/%40braintrust%2Fbrowser.svg)](https://www.npmjs.com/package/@braintrust/browser) | [integrations/browser-js/README.md](integrations/browser-js/README.md) | | `@braintrust/otel` | OpenTelemetry span processor and compatibility helpers for Braintrust tracing. | [![npm: @braintrust/otel](https://img.shields.io/npm/v/%40braintrust%2Fotel.svg)](https://www.npmjs.com/package/@braintrust/otel) | [integrations/otel-js/README.md](integrations/otel-js/README.md) | | `@braintrust/temporal` | Temporal client/worker plugin and workflow interceptors for Braintrust tracing. | [![npm: @braintrust/temporal](https://img.shields.io/npm/v/%40braintrust%2Ftemporal.svg)](https://www.npmjs.com/package/@braintrust/temporal) | [integrations/temporal-js/README.md](integrations/temporal-js/README.md) | ## CLI `bt` -The `bt` cli is installed alongside `braintrust`, use it with `npx bt` or `pnpm exec bt`. -See the [documentation](https://www.braintrust.dev/docs/reference/cli/quickstart). +The `bt` CLI is installed separately from `braintrust`. +See the standalone installation instructions in the [CLI quickstart](https://www.braintrust.dev/docs/reference/cli/quickstart). ## Documentation diff --git a/js/README.md b/js/README.md index 052250d14..0249838d3 100644 --- a/js/README.md +++ b/js/README.md @@ -14,6 +14,9 @@ Install the SDK: npm install braintrust ``` +The `bt` CLI is installed separately from the SDK. +See the standalone installation instructions in the [CLI quickstart](https://www.braintrust.dev/docs/reference/cli/quickstart). + ## Quickstart Run a simple experiment (replace `YOUR_API_KEY` with your Braintrust API key): diff --git a/js/bin/bt b/js/bin/bt deleted file mode 100755 index 5741f84d4..000000000 --- a/js/bin/bt +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -// Runtime launcher for the `bt` CLI. Resolves the platform-specific binary -// (installed via the matching `@braintrust/bt-*` optionalDependency, or -// downloaded by `scripts/install.js` as a fallback) and forwards argv, -// stdio, signals, and the exit code. - -const childProcess = require("node:child_process"); -const { getBinaryPath } = require("../scripts/bt-helper"); - -let binaryPath; -try { - binaryPath = getBinaryPath(); -} catch (err) { - console.error(err.message); - process.exit(1); -} - -const child = childProcess - .spawn(binaryPath, process.argv.slice(2), { - stdio: "inherit", - windowsHide: true, - }) - .on("error", (err) => { - console.error(err); - process.exit(1); - }) - .on("exit", (code, signal) => { - if (signal) { - // Detach our forwarding listener so the re-raised signal hits Node's - // default handler; otherwise we'd intercept it again and exit 0. - process.removeAllListeners(signal); - process.kill(process.pid, signal); - return; - } - process.exit(code ?? 1); - }); - -process.on("SIGTERM", () => child.kill("SIGTERM")); -process.on("SIGINT", () => child.kill("SIGINT")); diff --git a/js/package.json b/js/package.json index d4e150877..27b87cfa5 100644 --- a/js/package.json +++ b/js/package.json @@ -18,9 +18,6 @@ "./dist/index.mjs": "./dist/browser.mjs", "./dist/index.d.mts": "./dist/browser.d.mts" }, - "bin": { - "bt": "./bin/bt" - }, "exports": { "./package.json": "./package.json", ".": { @@ -111,14 +108,10 @@ }, "files": [ "dist/**/*", - "bin/bt", - "scripts/bt-helper.js", - "scripts/install.js", "NOTICE", "licenses/**/*" ], "scripts": { - "postinstall": "node ./scripts/install.js", "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsup", "check:typings": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", "watch": "tsup --watch", @@ -201,15 +194,6 @@ "peerDependencies": { "zod": "^3.25.34 || ^4.0" }, - "optionalDependencies": { - "@braintrust/bt-darwin-arm64": "0.12.0", - "@braintrust/bt-darwin-x64": "0.12.0", - "@braintrust/bt-linux-arm64": "0.12.0", - "@braintrust/bt-linux-x64": "0.12.0", - "@braintrust/bt-linux-x64-musl": "0.12.0", - "@braintrust/bt-win32-arm64": "0.12.0", - "@braintrust/bt-win32-x64": "0.12.0" - }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/", diff --git a/js/scripts/bt-helper.js b/js/scripts/bt-helper.js deleted file mode 100644 index 3502e5e07..000000000 --- a/js/scripts/bt-helper.js +++ /dev/null @@ -1,139 +0,0 @@ -"use strict"; - -// Shared helpers for locating the `bt` binary. Used by both `bin/bt` -// (the runtime launcher) and `scripts/install.js` (the postinstall -// fallback downloader). - -const fs = require("node:fs"); -const os = require("node:os"); -const path = require("node:path"); - -const BINARY_DISTRIBUTIONS = [ - { packageName: "@braintrust/bt-darwin-arm64", subpath: "bin/bt" }, - { packageName: "@braintrust/bt-darwin-x64", subpath: "bin/bt" }, - { packageName: "@braintrust/bt-linux-arm64", subpath: "bin/bt" }, - { packageName: "@braintrust/bt-linux-x64", subpath: "bin/bt" }, - { packageName: "@braintrust/bt-linux-x64-musl", subpath: "bin/bt" }, - { packageName: "@braintrust/bt-win32-arm64", subpath: "bin/bt.exe" }, - { packageName: "@braintrust/bt-win32-x64", subpath: "bin/bt.exe" }, -]; - -function detectLibc() { - if (process.platform !== "linux") return null; - try { - const report = process.report && process.report.getReport(); - if (report && report.header && report.header.glibcVersionRuntime) { - return "glibc"; - } - return "musl"; - } catch { - return "glibc"; - } -} - -function binaryName() { - return process.platform === "win32" ? "bt.exe" : "bt"; -} - -function getDistributionForThisPlatform() { - const arch = os.arch(); - const platform = os.platform(); - const subpath = `bin/${binaryName()}`; - - let packageName; - if (platform === "darwin") { - if (arch === "arm64") packageName = "@braintrust/bt-darwin-arm64"; - else if (arch === "x64") packageName = "@braintrust/bt-darwin-x64"; - } else if (platform === "linux") { - if (arch === "arm64") { - packageName = "@braintrust/bt-linux-arm64"; - } else if (arch === "x64") { - packageName = - detectLibc() === "musl" - ? "@braintrust/bt-linux-x64-musl" - : "@braintrust/bt-linux-x64"; - } - } else if (platform === "win32") { - if (arch === "arm64") packageName = "@braintrust/bt-win32-arm64"; - else if (arch === "x64") packageName = "@braintrust/bt-win32-x64"; - } - - return { packageName, subpath }; -} - -function throwUnsupportedPlatformError() { - throw new Error( - `Unsupported operating system or architecture! The bt CLI does not work on ${process.platform}-${process.arch}. - -bt supports: -- macOS (darwin) on arm64 and x64 -- Linux on arm64 and x64 (glibc and musl) -- Windows on arm64 and x64`, - ); -} - -// Constructed indirectly so bundlers (e.g. @vercel/nft) don't statically -// detect the fallback binary path as an asset to trace. -function getFallbackBinaryPath() { - const parts = [__dirname, binaryName()]; - return path.resolve(...parts); -} - -function getBinaryPath() { - if (process.env.BT_BINARY_PATH) { - return process.env.BT_BINARY_PATH; - } - - const { packageName, subpath } = getDistributionForThisPlatform(); - - if (packageName === undefined) { - throwUnsupportedPlatformError(); - } - - // Prefer the optional dep so a stale fallback from a prior - // `--omit=optional` install can't shadow a newer optional dep on upgrade. - try { - return require.resolve(`${packageName}/${subpath}`); - } catch (e) { - const fallbackBinaryPath = getFallbackBinaryPath(); - if (fs.existsSync(fallbackBinaryPath)) { - return fallbackBinaryPath; - } - - const otherInstalled = BINARY_DISTRIBUTIONS.find((dist) => { - try { - require.resolve(`${dist.packageName}/${dist.subpath}`); - return true; - } catch { - return false; - } - }); - - // Error messages inspired by esbuild: - // https://github.com/evanw/esbuild/blob/f3d535262e3998d845d0f102b944ecd5a9efda57/lib/npm/node-platform.ts#L150 - if (otherInstalled) { - throw new Error( - `bt binary for this platform/architecture not found! - -The "${otherInstalled.packageName}" package is installed, but for the current platform you should have the "${packageName}" package installed instead. This usually happens if "braintrust" is installed on one platform (for example macOS or Windows) and the "node_modules" folder is then reused on another (for example Linux in Docker). - -To fix this, avoid copying the "node_modules" folder, and instead freshly install your dependencies on the target system. You can also configure your package manager to install the right package. For example, yarn has the "supportedArchitectures" feature: https://yarnpkg.com/configuration/yarnrc/#supportedArchitecture.`, - ); - } - - throw new Error( - `bt binary for this platform/architecture not found! - -It seems like none of the "braintrust" package's optional dependencies got installed. Please make sure your package manager is configured to install optional dependencies. If you are using npm, don't set the "--no-optional", "--ignore-optional", or "--omit=optional" flags. The "braintrust" package needs the "optionalDependencies" feature in order to install the bt binary.`, - ); - } -} - -module.exports = { - BINARY_DISTRIBUTIONS, - binaryName, - getBinaryPath, - getDistributionForThisPlatform, - getFallbackBinaryPath, - throwUnsupportedPlatformError, -}; diff --git a/js/scripts/install.js b/js/scripts/install.js deleted file mode 100644 index 0f200f140..000000000 --- a/js/scripts/install.js +++ /dev/null @@ -1,153 +0,0 @@ -"use strict"; - -// Postinstall script for the `bt` CLI binary. -// -// The native binary ships in a per-platform `@braintrust/bt-*` package listed -// in `optionalDependencies`; npm/pnpm install only the one matching the host. -// If a package manager is run with `--no-optional`, `--ignore-optional`, or -// `--omit=optional`, none of those packages get installed and `bt` would be -// unusable. As a workaround, we manually fetch the matching tarball from the -// npm registry and extract the binary so the launcher can find it. - -const fs = require("node:fs"); -const https = require("node:https"); -const path = require("node:path"); -const zlib = require("node:zlib"); - -const helper = require("./bt-helper"); -const pkg = require("../package.json"); - -if (process.env.BT_SKIP_DOWNLOAD === "1") { - console.log( - "bt: skipping post-install binary download because BT_SKIP_DOWNLOAD=1 is set.", - ); - process.exit(0); -} - -const { packageName, subpath } = helper.getDistributionForThisPlatform(); - -if (packageName === undefined) { - // Don't fail the install; the launcher will surface the unsupported-platform - // error if/when the user actually tries to run `bt`. - console.error( - `bt: no prebuilt binary available for ${process.platform}-${process.arch}; the bt CLI will not be available.`, - ); - process.exit(0); -} - -try { - require.resolve(`${packageName}/${subpath}`); - // Optional dependency was installed successfully. Nothing to do. - process.exit(0); -} catch (e) { - // Fall through to the manual download path below. - console.log( - `bt: failed to locate the "${packageName}" package after installation. - -This can happen if you use an option to disable optional dependencies during installation, like "--no-optional", "--ignore-optional", or "--omit=optional". The "braintrust" package uses the "optionalDependencies" package.json feature to install the correct bt binary for your platform and operating system. This post-install script will now try to work around that by manually downloading the bt binary from the npm registry. If this fails, you need to remove the "--no-optional", "--ignore-optional", and "--omit=optional" flags for bt to work.`, - ); -} - -const version = (pkg.optionalDependencies || {})[packageName]; -if (!version) { - // Don't fail the parent install: the SDK works without `bt`, and the - // launcher errors clearly if it's actually invoked. - console.error( - `bt: cannot determine which version of "${packageName}" to download — it is not listed in the "braintrust" package's optionalDependencies. The bt CLI will not be available; the rest of the braintrust SDK is unaffected.`, - ); - process.exit(0); -} - -function fetchBuffer(url, redirectsRemaining = 5) { - return new Promise((resolve, reject) => { - https - .get(url, (response) => { - const { statusCode = 0, headers } = response; - if (statusCode >= 200 && statusCode < 300) { - const chunks = []; - response.on("data", (chunk) => chunks.push(chunk)); - response.on("end", () => resolve(Buffer.concat(chunks))); - response.on("error", reject); - return; - } - if ( - statusCode >= 300 && - statusCode < 400 && - headers.location && - redirectsRemaining > 0 - ) { - response.resume(); - fetchBuffer(headers.location, redirectsRemaining - 1).then( - resolve, - reject, - ); - return; - } - response.resume(); - reject( - new Error( - `npm registry responded with status code ${statusCode} when downloading ${url}`, - ), - ); - }) - .on("error", reject); - }); -} - -// Extracts a single file from an uncompressed tar archive. Tar archives are -// organized in 512-byte blocks: a header block (file name in bytes 0-99, -// file size in bytes 124-135 as an octal string) followed by data blocks -// padded out to the next multiple of 512. -function extractFileFromTarball(tarball, target) { - let offset = 0; - while (offset + 512 <= tarball.length) { - const header = tarball.subarray(offset, offset + 512); - offset += 512; - const fileName = header.toString("utf-8", 0, 100).replace(/\0.*/g, ""); - if (!fileName) break; - const fileSize = parseInt( - header.toString("utf-8", 124, 136).replace(/\0.*/g, ""), - 8, - ); - if (fileName === target) { - return tarball.subarray(offset, offset + fileSize); - } - offset = (offset + fileSize + 511) & ~511; - } - return null; -} - -async function downloadFallback() { - // npm tarball URLs look like: - // https://registry.npmjs.org///-/-.tgz - // where is the unscoped package name. - const tarballName = packageName.split("/").pop(); - const url = `https://registry.npmjs.org/${packageName}/-/${tarballName}-${version}.tgz`; - console.log(`bt: downloading ${packageName}@${version} from ${url}`); - - const gzipped = await fetchBuffer(url); - const tarball = zlib.gunzipSync(gzipped); - const binary = extractFileFromTarball(tarball, `package/${subpath}`); - - if (!binary) { - throw new Error( - `could not find "package/${subpath}" inside ${packageName}@${version} tarball`, - ); - } - - const fallbackBinaryPath = helper.getFallbackBinaryPath(); - fs.mkdirSync(path.dirname(fallbackBinaryPath), { recursive: true }); - fs.writeFileSync(fallbackBinaryPath, binary); - fs.chmodSync(fallbackBinaryPath, 0o755); - console.log(`bt: installed fallback binary at ${fallbackBinaryPath}`); -} - -downloadFallback().catch((err) => { - // Don't fail the parent install: airgapped/proxied CI may not reach the - // npm registry, and the SDK works without `bt`. - console.error( - `bt: failed to download fallback binary for ${packageName}@${version}: ${err.message} -The bt CLI will not be available; the rest of the braintrust SDK is unaffected.`, - ); - process.exit(0); -}); diff --git a/js/src/node/index.ts b/js/src/node/index.ts index e4a44adfe..7cf4ad1b1 100644 --- a/js/src/node/index.ts +++ b/js/src/node/index.ts @@ -42,10 +42,11 @@ * }); * ``` * - * Finally, run the script with `npx bt eval hello.eval.ts`. + * Install the `bt` CLI separately using the standalone installation instructions in the + * [CLI quickstart](https://www.braintrust.dev/docs/reference/cli/quickstart), then run the script: * * ```bash - * BRAINTRUST_API_KEY= npx bt eval hello.eval.ts + * BRAINTRUST_API_KEY= bt eval hello.eval.ts * ``` * * @module braintrust diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a36fd5d83..0ef57e08d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -421,28 +421,6 @@ importers: zod: specifier: ^3.25.34 version: 3.25.76 - optionalDependencies: - '@braintrust/bt-darwin-arm64': - specifier: 0.12.0 - version: 0.12.0 - '@braintrust/bt-darwin-x64': - specifier: 0.12.0 - version: 0.12.0 - '@braintrust/bt-linux-arm64': - specifier: 0.12.0 - version: 0.12.0 - '@braintrust/bt-linux-x64': - specifier: 0.12.0 - version: 0.12.0 - '@braintrust/bt-linux-x64-musl': - specifier: 0.12.0 - version: 0.12.0 - '@braintrust/bt-win32-arm64': - specifier: 0.12.0 - version: 0.12.0 - '@braintrust/bt-win32-x64': - specifier: 0.12.0 - version: 0.12.0 js/src/wrappers/vitest: devDependencies: @@ -482,44 +460,6 @@ packages: resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} - '@braintrust/bt-darwin-arm64@0.12.0': - resolution: {integrity: sha512-mY6VW/3VwcOQOGN8sYHS6F0xzHTFwgZcNlj7zlQttI6OXOCGt/bhonGIqd03QBhxmj0M31ymSS7TqSeCK/RYIQ==} - cpu: [arm64] - os: [darwin] - - '@braintrust/bt-darwin-x64@0.12.0': - resolution: {integrity: sha512-woyRyDv2DfCF8+von+3X9f1cddAbFnKLSfCbEkrBQVc0ZsAM60as8nehYv7DkafJF/67yKFx/sUraV65sukesQ==} - cpu: [x64] - os: [darwin] - - '@braintrust/bt-linux-arm64@0.12.0': - resolution: {integrity: sha512-J9/7f3EIMKmFmSSQHQnAQLpUgB8YJENrOlkABjlTprBgsIYVgz7tSH7yg2P3ur+2Jem7PpGnrDxHGh/UjRfjsg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@braintrust/bt-linux-x64-musl@0.12.0': - resolution: {integrity: sha512-KnLgENOoztBXcH+mLFJe4bYzi67kSOuELOQrm4Hler35GjgaBMI1fFLIUjKRPAmyT9VIhBMhy1ICfGEFLueMEQ==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@braintrust/bt-linux-x64@0.12.0': - resolution: {integrity: sha512-HJFbUl3HYYY1Ivw8OYBeIMcIsU9r7+fC9BzLWB5FdH7881ToKee2wbbLZssMCxFbMVeUxzEo+SzGD/1Z9Gk9CA==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@braintrust/bt-win32-arm64@0.12.0': - resolution: {integrity: sha512-+7PkdBAmiqEJsWteGHP/Zs62F75PkHPA8m/ej4TOz/mHGKulUU0bZibw91KRqIB7J7jGi5ItvCiqkiGaLKLnyw==} - cpu: [arm64] - os: [win32] - - '@braintrust/bt-win32-x64@0.12.0': - resolution: {integrity: sha512-Q0c+pVUGm82n/7N8QJ5s6j/G/Q0GFzqOaTipHjkmElLbAOOEcfRH/uljdtIPNBiEQcrzqirS0GmOgiFkeQ3Txg==} - cpu: [x64] - os: [win32] - '@changesets/apply-release-plan@7.1.0': resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} @@ -3843,27 +3783,6 @@ snapshots: '@babel/runtime@7.28.4': {} - '@braintrust/bt-darwin-arm64@0.12.0': - optional: true - - '@braintrust/bt-darwin-x64@0.12.0': - optional: true - - '@braintrust/bt-linux-arm64@0.12.0': - optional: true - - '@braintrust/bt-linux-x64-musl@0.12.0': - optional: true - - '@braintrust/bt-linux-x64@0.12.0': - optional: true - - '@braintrust/bt-win32-arm64@0.12.0': - optional: true - - '@braintrust/bt-win32-x64@0.12.0': - optional: true - '@changesets/apply-release-plan@7.1.0': dependencies: '@changesets/config': 3.1.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dd63d0ea2..ae72bc1f8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,8 +8,6 @@ packages: blockExoticSubdeps: true minimumReleaseAge: 4320 # 3 days (in minutes) -minimumReleaseAgeExclude: - - "@braintrust/bt-*" # bt binary packages, published in lockstep with braintrust trustPolicy: no-downgrade # Ignore the check for packages published more than 30 days ago (pnpm 10.27+) # Useful for older packages that pre-date provenance support From fb384185380e5f741c8835fb93c313e45a5b05a0 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 14 Oct 2026 16:10:20 +0200 Subject: [PATCH 14/19] feat!: Add `BRAINTRUST_CAPTURE_ATTACHMENTS` env var option and `captureAttachments` logger option for all integrations (#2519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref [braintrustdata/braintrust-sdk-javascript#1841](https://github.com/braintrustdata/braintrust-sdk-javascript/issues/1841) Targets `v4` (#2406). - Make automatic attachment capture opt-in across all integrations using the existing `BRAINTRUST_CAPTURE_ATTACHMENTS` environment variable; previously, integrations applied this setting inconsistently. - Add `initLogger({ captureAttachments: true | false })` to override the environment setting. Loggers created with `setCurrent: false` keep their own policy, independent of the global logger. - When capture is disabled, omit inline media and skip unnecessary decoding, copying, and buffering. Preserve text, metadata, metrics, remote URLs, and explicitly logged attachments. - Keep the owning logger’s policy throughout asynchronous calls and streaming responses, with unit, integration, and replay tests covering these behaviors. Validation: build, type checks, lint, and 628 focused unit tests pass. The affected replay e2e scenarios pass across three runs; production forwarding and generated Braintrust links were verified. The full replay suite also hits existing v4 failures in the Git timestamp fixture and HarnessAgent fixture; both were reproduced on an untouched v4 checkout. --- .changeset/light-plants-return.md | 6 + e2e/helpers/attachment-capture-assertions.ts | 67 ++ e2e/helpers/attachment-capture-scenario.mjs | 125 ++ .../ai-sdk-instrumentation/scenario.test.ts | 2 + .../scenario.capture.mjs | 10 + .../scenario.test.ts | 21 +- .../scenario.test.ts | 1 + .../scenario.test.ts | 10 +- .../scenario.test.ts | 10 +- .../groq-instrumentation/scenario.test.ts | 2 + .../scenario.capture.mjs | 10 + .../openai-instrumentation/scenario.test.ts | 11 + .../scenario.test.ts | 10 +- integrations/deepseek-harness/README.md | 12 +- .../deepseek-harness/src/index.test.ts | 190 +-- integrations/deepseek-harness/src/index.ts | 9 + js/src/attachment-capture.test.ts | 1024 +++++++++++++++++ js/src/exports.ts | 1 + .../core/channel-tracing.test.ts | 30 + .../instrumentation/core/channel-tracing.ts | 36 +- .../providers/ai-sdk-instrumentation.test.ts | 3 + .../providers/ai-sdk-instrumentation.ts | 147 ++- .../providers/ai-sdk-v7-telemetry.test.ts | 56 + .../anthropic-instrumentation.test.ts | 41 +- .../providers/anthropic-instrumentation.ts | 19 +- .../elevenlabs-instrumentation.test.ts | 89 +- .../providers/elevenlabs-instrumentation.ts | 36 +- .../providers/eve-instrumentation.test.ts | 583 +++++----- .../providers/eve-instrumentation.ts | 78 +- .../google-genai-instrumentation.test.ts | 172 ++- .../providers/google-genai-instrumentation.ts | 142 ++- ...ogle-generative-ai-instrumentation.test.ts | 47 +- .../google-generative-ai-instrumentation.ts | 11 +- .../providers/groq-instrumentation.ts | 25 +- .../providers/ollama-instrumentation.test.ts | 4 +- .../providers/ollama-instrumentation.ts | 12 +- .../openai-agents-trace-processor.ts | 25 +- .../providers/openai-instrumentation.test.ts | 60 +- .../providers/openai-instrumentation.ts | 49 +- .../instrumentation/providers/openai-media.ts | 11 +- .../providers/openai-span-data.ts | 23 +- .../pi-coding-agent-instrumentation.test.ts | 17 +- .../pi-coding-agent-instrumentation.ts | 56 +- .../strands-agent-sdk-instrumentation.test.ts | 30 +- .../strands-agent-sdk-instrumentation.ts | 86 +- .../voyageai-instrumentation.test.ts | 1 + js/src/logger.ts | 60 +- js/src/vendor-sdk-types/openai-common.ts | 1 + .../ai-sdk/normalize-logged-output.ts | 8 +- js/src/wrappers/ai-sdk/telemetry.ts | 25 +- js/src/wrappers/attachment-utils.test.ts | 96 +- js/src/wrappers/attachment-utils.ts | 252 +++- js/src/wrappers/groq.test.ts | 80 +- js/tests/provider-wrappers.test.ts | 6 +- 54 files changed, 3197 insertions(+), 741 deletions(-) create mode 100644 .changeset/light-plants-return.md create mode 100644 e2e/helpers/attachment-capture-assertions.ts create mode 100644 e2e/helpers/attachment-capture-scenario.mjs create mode 100644 e2e/scenarios/anthropic-instrumentation/scenario.capture.mjs create mode 100644 e2e/scenarios/openai-instrumentation/scenario.capture.mjs create mode 100644 js/src/attachment-capture.test.ts diff --git a/.changeset/light-plants-return.md b/.changeset/light-plants-return.md new file mode 100644 index 000000000..3f1a80f76 --- /dev/null +++ b/.changeset/light-plants-return.md @@ -0,0 +1,6 @@ +--- +"@braintrust/deepseek-harness": major +"braintrust": major +--- + +feat!: Add `BRAINTRUST_CAPTURE_ATTACHMENTS` env var option and `captureAttachments` logger option for all integrations diff --git a/e2e/helpers/attachment-capture-assertions.ts b/e2e/helpers/attachment-capture-assertions.ts new file mode 100644 index 000000000..122a944c6 --- /dev/null +++ b/e2e/helpers/attachment-capture-assertions.ts @@ -0,0 +1,67 @@ +import { expect, test } from "vitest"; +import { withScenarioHarness } from "./scenario-harness"; +import { findLatestSpan } from "./trace-selectors"; +import { captureCases } from "./attachment-capture-scenario.mjs"; + +export function defineAttachmentCaptureTests(options: { + scenarioDir: string; + originalScenarioDir: string; + provider: "openai" | "anthropic"; + packageName: string; + variantKey: string; +}) { + for (const mode of ["wrapped", "auto"]) { + test(`attachment capture policies (${options.variantKey}, ${mode})`, async () => { + await withScenarioHarness(async (harness) => { + await harness.runNodeScenarioDir({ + scenarioDir: options.scenarioDir, + entry: "scenario.capture.mjs", + nodeArgs: mode === "auto" ? ["--import", "braintrust/hook.mjs"] : [], + env: { + CAPTURE_PACKAGE_NAME: options.packageName, + CAPTURE_WRAPPED: String(mode === "wrapped"), + BRAINTRUST_CAPTURE_ATTACHMENTS: "", + }, + runContext: { + variantKey: options.variantKey, + originalScenarioDir: options.originalScenarioDir, + // The same Chat Completions request works on all supported SDK majors. + // Its real-provider recording lives in the existing v6 media cassette. + ...(options.provider === "openai" + ? { cassette: { variantKey: "openai-v6" } } + : {}), + }, + timeoutMs: 180_000, + }); + const events = harness.events(); + for (const testCase of captureCases) { + const root = findLatestSpan( + events, + `attachment-capture-${testCase.name}`, + ); + expect(root).toBeDefined(); + const children = events.filter( + (event) => + event.row.root_span_id === root?.row.root_span_id && + event.span.id !== root?.span.id, + ); + expect(children.length).toBeGreaterThan(0); + const payload = JSON.stringify(children); + if (testCase.enabled) + expect(payload).toContain("braintrust_attachment"); + else { + expect(payload).not.toContain("braintrust_attachment"); + expect(payload).not.toContain('"file_data":'); + expect(payload).not.toContain('"b64_json":'); + expect(payload).not.toContain('"data":'); + expect(payload).not.toContain("data:image/png;base64,"); + } + expect(payload).toContain("completion_tokens"); + expect(children.some((event) => event.row.output !== undefined)).toBe( + true, + ); + } + }); + }, 240_000); + } +} diff --git a/e2e/helpers/attachment-capture-scenario.mjs b/e2e/helpers/attachment-capture-scenario.mjs new file mode 100644 index 000000000..f8145696f --- /dev/null +++ b/e2e/helpers/attachment-capture-scenario.mjs @@ -0,0 +1,125 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { initLogger, wrapOpenAI, wrapAnthropic } from "braintrust"; +import { MINIMAL_PNG_BASE64 } from "./media-fixtures.mjs"; +import { scopedName } from "./provider-runtime.mjs"; + +export const captureCases = [ + { name: "default", env: "", enabled: false }, + { name: "local-default", env: "", global: true, local: true, enabled: false }, + { + name: "local-enabled", + env: "false", + global: false, + local: true, + option: true, + enabled: true, + }, + { + name: "local-disabled", + env: "true", + global: true, + local: true, + option: false, + enabled: false, + }, + { + name: "local-environment", + env: "true", + global: false, + local: true, + enabled: true, + }, + { name: "global-enabled", env: "false", option: true, enabled: true }, + { name: "global-disabled", env: "true", option: false, enabled: false }, +]; + +export async function runAttachmentCaptureScenario(provider, scenarioUrl, sdk) { + const wrapped = process.env.CAPTURE_WRAPPED === "true"; + let request; + if (provider === "anthropic") { + const raw = new sdk.default({ + apiKey: process.env.ANTHROPIC_API_KEY, + baseURL: process.env.ANTHROPIC_BASE_URL, + }); + const client = wrapped ? wrapAnthropic(raw) : raw; + const data = ( + await readFile(new URL("./test-image.png", scenarioUrl)) + ).toString("base64"); + request = async () => { + const result = await client.messages.create({ + model: "claude-haiku-4-5", + max_tokens: 32, + temperature: 0, + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Describe the attached image in one short sentence.", + }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data }, + }, + ], + }, + ], + }); + assert.ok(result.content.length > 0); + }; + } else if (provider === "openai") { + const raw = new sdk.default({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL, + }); + const client = wrapped ? wrapOpenAI(raw) : raw; + request = async () => { + const result = await client.chat.completions.create({ + model: "gpt-4o-mini-2024-07-18", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Describe this image in three words or fewer.", + }, + { + type: "image_url", + image_url: { + url: `data:image/png;base64,${MINIMAL_PNG_BASE64}`, + }, + }, + ], + }, + ], + max_tokens: 24, + temperature: 0, + }); + assert.ok(result.choices[0].message.content); + }; + } + const projectName = scopedName("tmp-luca-attachment-capture"); + for (const testCase of captureCases) { + process.env.BRAINTRUST_CAPTURE_ATTACHMENTS = testCase.env; + if (testCase.local) + initLogger({ projectName, captureAttachments: testCase.global }); + const logger = initLogger({ + projectName, + setCurrent: !testCase.local, + captureAttachments: testCase.option, + }); + await logger.traced(request, { + name: `attachment-capture-${testCase.name}`, + event: { + metadata: { + scenario: `${provider}-instrumentation`, + testRunId: process.env.BRAINTRUST_E2E_RUN_ID, + }, + }, + }); + await logger.flush(); + } +} diff --git a/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts index 6fd7ff261..616ea11d3 100644 --- a/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts @@ -55,6 +55,7 @@ describeVariants("variants", () => { await runScenarioDir({ entry: scenario.wrapperEntry, env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", AI_SDK_PACKAGE_NAME: scenario.packageName, AI_SDK_OPENAI_PACKAGE_NAME: scenario.openaiModuleName, AI_SDK_SUPPORTS_EVALUATE_STRING_MODEL: String( @@ -116,6 +117,7 @@ describeVariants("variants", () => { await runNodeScenarioDir({ entry: scenario.autoEntry, env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", AI_SDK_PACKAGE_NAME: scenario.packageName, AI_SDK_OPENAI_PACKAGE_NAME: scenario.openaiModuleName, AI_SDK_SUPPORTS_EVALUATE_STRING_MODEL: String( diff --git a/e2e/scenarios/anthropic-instrumentation/scenario.capture.mjs b/e2e/scenarios/anthropic-instrumentation/scenario.capture.mjs new file mode 100644 index 000000000..44a743716 --- /dev/null +++ b/e2e/scenarios/anthropic-instrumentation/scenario.capture.mjs @@ -0,0 +1,10 @@ +import { runMain } from "../../helpers/provider-runtime.mjs"; +import { runAttachmentCaptureScenario } from "../../helpers/attachment-capture-scenario.mjs"; + +runMain(async () => + runAttachmentCaptureScenario( + "anthropic", + import.meta.url, + await import(process.env.CAPTURE_PACKAGE_NAME), + ), +); diff --git a/e2e/scenarios/anthropic-instrumentation/scenario.test.ts b/e2e/scenarios/anthropic-instrumentation/scenario.test.ts index f2c7e3cf6..32dc02950 100644 --- a/e2e/scenarios/anthropic-instrumentation/scenario.test.ts +++ b/e2e/scenarios/anthropic-instrumentation/scenario.test.ts @@ -1,3 +1,4 @@ +import { defineAttachmentCaptureTests } from "../../helpers/attachment-capture-assertions"; import { describe } from "vitest"; import { prepareScenarioDir, @@ -52,7 +53,10 @@ describe.concurrent("variants", () => { runScenario: async ({ runScenarioDir }) => { await runScenarioDir({ entry: scenario.wrapperEntry, - env: { ANTHROPIC_PACKAGE_NAME: scenario.dependencyName }, + env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + ANTHROPIC_PACKAGE_NAME: scenario.dependencyName, + }, runContext: { variantKey: scenario.snapshotName, originalScenarioDir, @@ -76,7 +80,10 @@ describe.concurrent("variants", () => { runScenario: async ({ runNodeScenarioDir }) => { await runNodeScenarioDir({ entry: scenario.autoEntry, - env: { ANTHROPIC_PACKAGE_NAME: scenario.dependencyName }, + env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + ANTHROPIC_PACKAGE_NAME: scenario.dependencyName, + }, nodeArgs: ["--import", "braintrust/hook.mjs"], runContext: { variantKey: scenario.snapshotName, @@ -98,3 +105,13 @@ describe.concurrent("variants", () => { }); } }); + +for (const scenario of anthropicScenarios) { + defineAttachmentCaptureTests({ + scenarioDir, + originalScenarioDir, + provider: "anthropic", + packageName: scenario.dependencyName, + variantKey: scenario.snapshotName, + }); +} diff --git a/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts b/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts index e4d82876d..4dbecef9f 100644 --- a/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts +++ b/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts @@ -30,6 +30,7 @@ for (const variant of ["elevenlabs-v2", "elevenlabs-v2-latest"]) { ? ["--import", "braintrust/hook.mjs"] : [], env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", ELEVENLABS_PACKAGE_NAME: variant.replace( "elevenlabs-", "elevenlabs-sdk-", diff --git a/e2e/scenarios/google-genai-instrumentation/scenario.test.ts b/e2e/scenarios/google-genai-instrumentation/scenario.test.ts index 7863a372f..07bff02e8 100644 --- a/e2e/scenarios/google-genai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/google-genai-instrumentation/scenario.test.ts @@ -56,7 +56,10 @@ describe.concurrent("variants", () => { runScenario: async ({ runScenarioDir }) => { await runScenarioDir({ entry: scenario.wrapperEntry, - env: { GOOGLE_GENAI_PACKAGE_NAME: scenario.dependencyName }, + env: { + GOOGLE_GENAI_PACKAGE_NAME: scenario.dependencyName, + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + }, runContext: { variantKey: scenario.snapshotName, originalScenarioDir, @@ -75,7 +78,10 @@ describe.concurrent("variants", () => { runScenario: async ({ runNodeScenarioDir }) => { await runNodeScenarioDir({ entry: scenario.autoEntry, - env: { GOOGLE_GENAI_PACKAGE_NAME: scenario.dependencyName }, + env: { + GOOGLE_GENAI_PACKAGE_NAME: scenario.dependencyName, + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + }, nodeArgs: ["--import", "braintrust/hook.mjs"], runContext: { variantKey: scenario.snapshotName, diff --git a/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts b/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts index 043dae2f4..3d24ba178 100644 --- a/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts @@ -30,7 +30,10 @@ describe.concurrent("variants", () => { runScenario: async ({ runScenarioDir }) => { await runScenarioDir({ entry: "scenario.ts", - env: { GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName }, + env: { + GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName, + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + }, runContext: { originalScenarioDir, variantKey: scenario.snapshotName, @@ -50,7 +53,10 @@ describe.concurrent("variants", () => { await runNodeScenarioDir({ entry: "scenario.mjs", nodeArgs: ["--import", "braintrust/hook.mjs"], - env: { GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName }, + env: { + GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName, + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + }, runContext: { originalScenarioDir, variantKey: scenario.snapshotName, diff --git a/e2e/scenarios/groq-instrumentation/scenario.test.ts b/e2e/scenarios/groq-instrumentation/scenario.test.ts index be093c3d8..10641bbbc 100644 --- a/e2e/scenarios/groq-instrumentation/scenario.test.ts +++ b/e2e/scenarios/groq-instrumentation/scenario.test.ts @@ -44,6 +44,7 @@ describe.concurrent("variants", () => { await runScenarioDir({ entry: "scenario.ts", env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", GROQ_AUDIO_FILE: audioFile, GROQ_PACKAGE_NAME: scenario.dependencyName, }, @@ -66,6 +67,7 @@ describe.concurrent("variants", () => { await runNodeScenarioDir({ entry: "scenario.mjs", env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", GROQ_AUDIO_FILE: audioFile, GROQ_PACKAGE_NAME: scenario.dependencyName, }, diff --git a/e2e/scenarios/openai-instrumentation/scenario.capture.mjs b/e2e/scenarios/openai-instrumentation/scenario.capture.mjs new file mode 100644 index 000000000..780f3f1e5 --- /dev/null +++ b/e2e/scenarios/openai-instrumentation/scenario.capture.mjs @@ -0,0 +1,10 @@ +import { runMain } from "../../helpers/provider-runtime.mjs"; +import { runAttachmentCaptureScenario } from "../../helpers/attachment-capture-scenario.mjs"; + +runMain(async () => + runAttachmentCaptureScenario( + "openai", + import.meta.url, + await import(process.env.CAPTURE_PACKAGE_NAME), + ), +); diff --git a/e2e/scenarios/openai-instrumentation/scenario.test.ts b/e2e/scenarios/openai-instrumentation/scenario.test.ts index 951ef3379..dc1fa0e69 100644 --- a/e2e/scenarios/openai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/openai-instrumentation/scenario.test.ts @@ -1,3 +1,4 @@ +import { defineAttachmentCaptureTests } from "../../helpers/attachment-capture-assertions"; import { describe, it } from "vitest"; import { prepareScenarioDir, @@ -136,3 +137,13 @@ describe.concurrent("variants", () => { }); } }); + +for (const scenario of openaiScenarios) { + defineAttachmentCaptureTests({ + scenarioDir, + originalScenarioDir, + provider: "openai", + packageName: scenario.dependencyName, + variantKey: scenario.snapshotName, + }); +} diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts index 6d28b2730..cbcfd1167 100644 --- a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts @@ -49,7 +49,10 @@ describe.concurrent("variants", () => { runScenario: async ({ runScenarioDir }) => { await runScenarioDir({ entry: scenario.wrapperEntry, - env: { STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName }, + env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName, + }, runContext: { variantKey: scenario.variantKey, originalScenarioDir, @@ -68,7 +71,10 @@ describe.concurrent("variants", () => { runScenario: async ({ runNodeScenarioDir }) => { await runNodeScenarioDir({ entry: scenario.autoEntry, - env: { STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName }, + env: { + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName, + }, nodeArgs: ["--import", "braintrust/hook.mjs"], runContext: { variantKey: scenario.variantKey, diff --git a/integrations/deepseek-harness/README.md b/integrations/deepseek-harness/README.md index 339c40783..24f89e2e2 100644 --- a/integrations/deepseek-harness/README.md +++ b/integrations/deepseek-harness/README.md @@ -30,8 +30,10 @@ To override the defaults, add the following entry to the profile's ``` The optional configuration fields are `apiKey`, `projectName`, `metadata`, -`orgName`, and `appUrl`. `metadata` is added to every top-level user-turn trace, -and `projectName` defaults to `DeepSeek Harness`. You can set `apiKey` -under **Settings > Plugins** in Harness; it is declared as a secret field so -the settings UI handles it as a credential. A configured `apiKey` takes -precedence over the `BRAINTRUST_API_KEY` environment variable. +`orgName`, `appUrl`, and `captureAttachments`. `metadata` is added to every +top-level user-turn trace, and `projectName` defaults to `DeepSeek Harness`. +You can set `apiKey` under **Settings > Plugins** in Harness; it is declared as +a secret field so the settings UI handles it as a credential. A configured +`apiKey` takes precedence over the `BRAINTRUST_API_KEY` environment variable. +Set `captureAttachments: true` to upload Harness images as attachments; when it +is omitted, the `BRAINTRUST_CAPTURE_ATTACHMENTS` environment variable decides. diff --git a/integrations/deepseek-harness/src/index.test.ts b/integrations/deepseek-harness/src/index.test.ts index 6d2c6dcf8..ca318eee3 100644 --- a/integrations/deepseek-harness/src/index.test.ts +++ b/integrations/deepseek-harness/src/index.test.ts @@ -49,10 +49,12 @@ const mock = vi.hoisted(() => { flush: vi.fn(async () => undefined), }; const initLogger = vi.fn(() => logger); + const captureAttachmentsEnabled = vi.fn(() => true); return { Attachment: MockAttachment, attachments, + captureAttachmentsEnabled, initLogger, logger, reset() { @@ -61,12 +63,14 @@ const mock = vi.hoisted(() => { spans.length = 0; initLogger.mockClear(); logger.flush.mockClear(); + captureAttachmentsEnabled.mockReturnValue(true); }, spans, }; }); vi.mock("braintrust", () => ({ + _internalCaptureAttachmentsEnabled: mock.captureAttachmentsEnabled, Attachment: mock.Attachment, initLogger: mock.initLogger, NOOP_SPAN: {}, @@ -148,6 +152,14 @@ describe("DeepSeek Harness plugin", () => { expect(ConfigSchema.dict?.apiKey?.meta.role).toBe("secret"); }); + test("passes the attachment capture setting to its logger", () => { + createContext({ captureAttachments: true }); + + expect(mock.initLogger).toHaveBeenCalledWith( + expect.objectContaining({ captureAttachments: true }), + ); + }); + test("only logs the supported tool definition fields", async () => { const harness = createContext(); const stream = harness.listener("llm/stream")( @@ -228,88 +240,104 @@ describe("DeepSeek Harness plugin", () => { } }); - test("converts Harness image references to Braintrust attachments", async () => { - const harness = createContext(); - const currentSession = session(); - const image = { - attachmentId: "image-1", - mediaType: "image/png", - bytes: 3, - width: 1, - height: 1, - name: "diagram.png", - }; - const message = { - role: "user", - content: [ - { type: "text", text: "describe this" }, - { type: "image", attachment: image }, - ], - source: { kind: "user" }, - }; - - harness.listener("session/event")(currentSession, { - type: "turn/start", - data: { turn: 1 }, - }); - harness.listener("session/event")(currentSession, { - type: "user/message", - data: message, - }); - await consume( - harness.listener("llm/stream")( - { - provider: "replay", - model: "replay-model", - messages: [message], - sessionId: currentSession.id, - }, - () => - (async function* () { - yield { type: "finish", reason: { kind: "stop" } }; - })(), - ), - ); - await harness.listener("session/flush")(currentSession); - - expect(harness.readImage).toHaveBeenCalledOnce(); - expect(harness.readImage).toHaveBeenCalledWith(image); - expect(mock.attachments).toHaveLength(1); - expect(mock.attachments[0]?.params).toMatchObject({ - filename: "diagram.png", - contentType: "image/png", - }); - expect(new Uint8Array(mock.attachments[0]?.params.data ?? [])).toEqual( - new Uint8Array([1, 2, 3]), - ); + test.each([true, false])( + "processes Harness images with capture=%s", + async (captureAttachments) => { + mock.captureAttachmentsEnabled.mockReturnValue(captureAttachments); + const harness = createContext(); + const currentSession = session(); + const image = { + attachmentId: "image-1", + mediaType: "image/png", + bytes: 3, + width: 1, + height: 1, + name: "diagram.png", + }; + const message = { + role: "user", + content: [ + { type: "text", text: "describe this" }, + { type: "image", attachment: image }, + ], + source: { kind: "user" }, + }; - const expectedImagePart = { - type: "image_url", - image_url: { url: mock.attachments[0] }, - }; - const turn = mock.spans.find( - (span) => span.args.name === "deepseek_harness.turn", - ); - const model = mock.spans.find( - (span) => span.args.name === "deepseek_harness.step", - ); - expect(turn?.logs).toContainEqual({ - input: [ - { - role: "user", - content: [{ type: "text", text: "describe this" }, expectedImagePart], - }, - ], - }); - expect(model?.logs).toContainEqual({ - input: [ - { - role: "user", - content: [{ type: "text", text: "describe this" }, expectedImagePart], - }, - ], - }); - }); + harness.listener("session/event")(currentSession, { + type: "turn/start", + data: { turn: 1 }, + }); + harness.listener("session/event")(currentSession, { + type: "user/message", + data: message, + }); + await consume( + harness.listener("llm/stream")( + { + provider: "replay", + model: "replay-model", + messages: [message], + sessionId: currentSession.id, + }, + () => + (async function* () { + yield { type: "finish", reason: { kind: "stop" } }; + })(), + ), + ); + await harness.listener("session/flush")(currentSession); + + let expectedImagePart: Record; + if (captureAttachments) { + expect(harness.readImage).toHaveBeenCalledOnce(); + expect(harness.readImage).toHaveBeenCalledWith(image); + expect(mock.attachments).toHaveLength(1); + expect(mock.attachments[0]?.params).toMatchObject({ + filename: "diagram.png", + contentType: "image/png", + }); + expect(new Uint8Array(mock.attachments[0]?.params.data ?? [])).toEqual( + new Uint8Array([1, 2, 3]), + ); + expectedImagePart = { + type: "image_url", + image_url: { url: mock.attachments[0] }, + }; + } else { + expect(harness.readImage).not.toHaveBeenCalled(); + expect(mock.attachments).toHaveLength(0); + expectedImagePart = { type: "image", attachment: image }; + } + const turn = mock.spans.find( + (span) => span.args.name === "deepseek_harness.turn", + ); + const model = mock.spans.find( + (span) => span.args.name === "deepseek_harness.step", + ); + expect(turn?.logs).toContainEqual({ + input: [ + { + role: "user", + content: [ + { type: "text", text: "describe this" }, + expectedImagePart, + ], + }, + ], + }); + expect(model?.logs).toContainEqual({ + input: [ + { + role: "user", + content: [ + { type: "text", text: "describe this" }, + expectedImagePart, + ], + }, + ], + }); + }, + ); test("preserves Harness image references when attachment conversion fails", async () => { const harness = createContext(); diff --git a/integrations/deepseek-harness/src/index.ts b/integrations/deepseek-harness/src/index.ts index e790a35d8..4ce891edd 100644 --- a/integrations/deepseek-harness/src/index.ts +++ b/integrations/deepseek-harness/src/index.ts @@ -7,6 +7,7 @@ import type {} from "@deepseek-ai/dsh-session"; import type {} from "@deepseek-ai/dsh-tools"; import z from "@deepseek-ai/schemastery"; import { + _internalCaptureAttachmentsEnabled, Attachment, initLogger, NOOP_SPAN, @@ -49,6 +50,11 @@ export interface Config { orgName?: string; /** Optional Braintrust deployment URL. */ appUrl?: string; + /** + * Capture inline Harness images as attachments. + * Falls back to BRAINTRUST_CAPTURE_ATTACHMENTS when omitted. + */ + captureAttachments?: boolean; } export const Config: z = z.object({ @@ -57,6 +63,7 @@ export const Config: z = z.object({ metadata: z.dict(z.any()), orgName: z.string(), appUrl: z.string(), + captureAttachments: z.boolean(), }); type TokenMetrics = Record & { @@ -373,6 +380,7 @@ export function apply(ctx: Context, config: Config = {}): void { projectName: config.projectName ?? DEFAULT_PROJECT_NAME, ...(config.orgName ? { orgName: config.orgName } : {}), ...(config.appUrl ? { appUrl: config.appUrl } : {}), + captureAttachments: config.captureAttachments, setCurrent: false, }); const turns = new Map>(); @@ -394,6 +402,7 @@ export function apply(ctx: Context, config: Config = {}): void { const resolveImage = async ( ref: HarnessImageAttachmentRef, ): Promise => { + if (!_internalCaptureAttachmentsEnabled(logger)) return undefined; const attachmentStore = ( ctx as Context & { attachments?: HarnessAttachmentStore } ).attachments; diff --git a/js/src/attachment-capture.test.ts b/js/src/attachment-capture.test.ts new file mode 100644 index 000000000..95f09b368 --- /dev/null +++ b/js/src/attachment-capture.test.ts @@ -0,0 +1,1024 @@ +import { afterEach, beforeAll, beforeEach, expect, it, vi } from "vitest"; +import { + _internalCaptureAttachmentsEnabled, + Attachment, + BraintrustState, + CAPTURE_ATTACHMENTS, + ExternalAttachment, + startSpan, + _exportsForTestingOnly, + _internalGetGlobalState, + initLogger, +} from "./logger"; +import { configureNode } from "./node/config"; +import iso from "./isomorph"; +import { processInputAttachments } from "./wrappers/attachment-utils"; +import { processAttachmentsInInput } from "./instrumentation/providers/anthropic-instrumentation"; +import { + processAISDKCallInput, + processAISDKGenerateImageInput, + processAISDKGenerateImageOutput, + processAISDKOutput, + patchAISDKStreamingResult, +} from "./instrumentation/providers/ai-sdk-instrumentation"; +import { OpenAIAgentsTraceProcessor } from "./instrumentation/providers/openai-agents-trace-processor"; +import type { OpenAIAgentsSpan } from "./vendor-sdk-types/openai-agents"; +import { extractOllamaChatInput } from "./instrumentation/providers/ollama-instrumentation"; +import { openAIChannels } from "./instrumentation/providers/openai-channels"; + +configureNode(); +const state = _internalGetGlobalState(); +let background: ReturnType< + typeof _exportsForTestingOnly.useTestBackgroundLogger +>; +beforeAll(() => _exportsForTestingOnly.simulateLoginForTests()); +beforeEach(() => { + vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", undefined); + state.captureAttachments = undefined; + state.currentLogger = undefined; + background = _exportsForTestingOnly.useTestBackgroundLogger(); +}); +afterEach(async () => { + await background.drain(); + _exportsForTestingOnly.clearTestBackgroundLogger(); + state.captureAttachments = undefined; + state.currentLogger = undefined; + vi.restoreAllMocks(); + vi.unstubAllEnvs(); +}); +const loggerOptions = { + projectName: "tmp-luca-capture-tests", + projectId: "test-project-id", +}; +const inlineImage = { + type: "image_url", + image_url: { url: "data:image/png;base64,AQID" }, +}; + +it.each([false, true])( + "processes native OpenAI media with capture=%s", + (captureAttachments) => { + const input = [ + { + type: "input_image", + image_url: "data:image/png;base64,AQID", + detail: "low", + }, + { type: "input_image", image: "data:image/png;base64,AQID" }, + { type: "input_file", file_data: "AQID", filename: "document.pdf" }, + { type: "input_audio", input_audio: { data: "AQID", format: "wav" } }, + { type: "input_image", image_url: "https://example.com/image.png" }, + { type: "input_file", file_id: "file-123" }, + ]; + const result = processInputAttachments(input, captureAttachments); + expect(result).toEqual([ + { + type: "input_image", + detail: "low", + ...(captureAttachments ? { image_url: expect.any(Attachment) } : {}), + }, + ...(captureAttachments + ? [{ type: "input_image", image: expect.any(Attachment) }] + : []), + { + type: "input_file", + filename: "document.pdf", + ...(captureAttachments ? { file_data: expect.any(Attachment) } : {}), + }, + { + type: "input_audio", + input_audio: { + format: "wav", + ...(captureAttachments ? { data: expect.any(Attachment) } : {}), + }, + }, + ...input.slice(4), + ]); + expect(input[2].file_data).toBe("AQID"); + }, +); + +it.each([false, true])( + "applies the owning policy to AI SDK tool-result media (%s)", + async (captureAttachments) => { + initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + const local = initLogger({ + ...loggerOptions, + setCurrent: false, + captureAttachments, + }); + const media = ["image-data", "file-data", "media"].map((type) => ({ + type, + data: "AQID", + mediaType: "image/png", + })); + const references = [ + { type: "text", text: "Screenshot captured" }, + { type: "image-url", url: "https://example.com/image.png" }, + { type: "file-id", fileId: "file-123" }, + ]; + const toolResult = { + type: "tool-result", + toolCallId: "call-1", + toolName: "screenshot", + output: { type: "content", value: [...media, ...references] }, + }; + const messages = [{ role: "tool", content: [toolResult] }]; + const decode = vi.spyOn(globalThis, "atob"); + await local.traced(() => { + const input = processAISDKCallInput({ + model: { modelId: "model", provider: "test" }, + messages, + }).input; + const output = processAISDKOutput({ response: { messages } }, []); + const expected = [ + ...media.map(({ data: _data, ...part }) => ({ + ...part, + ...(captureAttachments ? { data: expect.any(Attachment) } : {}), + })), + ...references, + ]; + expect(input).toHaveProperty( + "messages.0.content.0.output.value", + expected, + ); + expect(output).toHaveProperty( + "response.messages.0.content.0.output.value", + expected, + ); + }); + if (!captureAttachments) expect(decode).not.toHaveBeenCalled(); + expect(toolResult.output.value).toEqual([...media, ...references]); + expect(media.every((part) => part.data === "AQID")).toBe(true); + }, +); + +it.each([false, true])( + "processes AI SDK media throughout output containers with capture=%s", + (captureAttachments) => { + initLogger({ ...loggerOptions, captureAttachments }); + const readBytes = vi.fn(() => "AQID"); + const generatedFile = { + mediaType: "image/png", + get base64() { + return readBytes(); + }, + }; + const file = { type: "file", file: generatedFile }; + const message = { + role: "assistant", + content: [ + { type: "file", mediaType: "image/png", data: "AQID" }, + { type: "text", text: "Done" }, + ], + }; + const result = processAISDKOutput( + { + content: [file], + steps: [{ content: [file], response: { messages: [message] } }], + response: { + messages: [message], + timestamp: new Date("2026-09-28T00:00:00Z"), + }, + usage: { inputTokens: 1, outputTokens: 2 } as Parameters< + typeof processAISDKOutput + >[0]["usage"], + }, + [], + ); + expect(result).toHaveProperty( + "content", + captureAttachments + ? [{ type: "file", file: expect.any(Attachment) }] + : [], + ); + expect(result).toHaveProperty( + "steps.0.content", + captureAttachments + ? [{ type: "file", file: expect.any(Attachment) }] + : [], + ); + for (const path of [ + "response.messages.0.content", + "steps.0.response.messages.0.content", + ]) { + expect(result).toHaveProperty(path, [ + { + type: "file", + mediaType: "image/png", + ...(captureAttachments ? { data: expect.any(Attachment) } : {}), + }, + { type: "text", text: "Done" }, + ]); + } + expect(result).toHaveProperty("usage", { inputTokens: 1, outputTokens: 2 }); + expect(result).toHaveProperty( + "response.timestamp", + "2026-09-28T00:00:00.000Z", + ); + if (!captureAttachments) expect(readBytes).not.toHaveBeenCalled(); + expect(message.content[0].data).toBe("AQID"); + }, +); + +it.each([false, true])( + "uses the active Agents processor's owning policy (%s)", + async (captureAttachments) => { + const logger = initLogger({ + ...loggerOptions, + setCurrent: false, + captureAttachments, + }); + const processor = new OpenAIAgentsTraceProcessor({ logger }); + const trace = { + type: "trace" as const, + traceId: "media-trace", + name: "media trace", + groupId: null, + }; + const span: OpenAIAgentsSpan = { + type: "trace.span", + traceId: trace.traceId, + spanId: "media-span", + parentId: null, + startedAt: null, + endedAt: null, + error: null, + spanData: { + type: "response", + _input: [ + { type: "input_image", image: "data:image/png;base64,AQID" }, + { type: "input_file", file: "data:application/pdf;base64,AQID" }, + { type: "input_file", file: "https://example.com/report.pdf" }, + { type: "input_file", file: { id: "file-123" } }, + { + type: "input_file", + file: { url: "https://example.com/report.pdf" }, + }, + ], + _response: { + output: [{ type: "image_generation_call", result: "AQID" }], + }, + }, + }; + await processor.onTraceStart(trace); + await processor.onSpanStart(span); + initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + await processor.onSpanEnd(span); + const rows = await background.drain(); + const output = rows.find( + (row) => "output" in row && row.output !== undefined, + ); + expect(output).toHaveProperty( + "output", + captureAttachments + ? [{ type: "image_generation_call", result: expect.any(Attachment) }] + : [], + ); + expect(output).toHaveProperty("input", [ + ...(captureAttachments + ? [ + { type: "input_image", image: expect.any(Attachment) }, + { type: "input_file", file: expect.any(Attachment) }, + ] + : []), + { type: "input_file", file: "https://example.com/report.pdf" }, + { type: "input_file", file: { id: "file-123" } }, + { type: "input_file", file: { url: "https://example.com/report.pdf" } }, + ]); + expect( + processor._traceSpans.get(trace.traceId)?.metadata.lastOutput, + ).toEqual(output && "output" in output ? output.output : undefined); + }, +); + +it.each([false, true])( + "keeps AI SDK streamed media on its owning policy (%s)", + async (captureAttachments) => { + const local = initLogger({ + ...loggerOptions, + setCurrent: false, + captureAttachments, + }); + const span = local.startSpan({ name: "streamed media" }); + const part = { + type: "file", + data: new Uint8Array([1, 2, 3]), + mediaType: "image/png", + }; + const result = { + fullStream: (async function* () { + yield part; + })(), + content: [part], + text: "Done", + }; + expect( + patchAISDKStreamingResult({ + defaultDenyOutputPaths: [], + endEvent: {}, + result, + span, + startTime: Date.now() / 1000, + }), + ).toBe(true); + initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + for await (const chunk of result.fullStream) expect(chunk).toBe(part); + const rows = await background.drain(); + expect(rows.find((row) => "output" in row)).toHaveProperty("output", { + content: [ + { + type: "file", + mediaType: "image/png", + ...(captureAttachments ? { data: expect.any(Attachment) } : {}), + }, + ], + text: "Done", + }); + expect(part.data).toEqual(new Uint8Array([1, 2, 3])); + }, +); + +it.each([false, true])( + "applies the active Agents policy to speech and transcription (%s)", + async (captureAttachments) => { + const local = initLogger({ + ...loggerOptions, + setCurrent: false, + captureAttachments, + }); + const processor = new OpenAIAgentsTraceProcessor({ logger: local }); + const trace = { + type: "trace" as const, + traceId: "audio-trace", + name: "audio trace", + groupId: null, + }; + await processor.onTraceStart(trace); + for (const type of ["speech", "transcription"] as const) { + const audio = { data: "AQID", format: "pcm" }; + const span: OpenAIAgentsSpan = { + type: "trace.span", + traceId: trace.traceId, + spanId: type, + parentId: null, + startedAt: null, + endedAt: null, + error: null, + spanData: + type === "speech" + ? { type, input: "Hello", output: audio } + : { type, input: audio, output: "Hello" }, + }; + await processor.onSpanStart(span); + initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + await processor.onSpanEnd(span); + const payload = (await background.drain()).find((row) => "output" in row); + expect(payload).toHaveProperty(type === "speech" ? "output" : "input", { + format: "pcm", + ...(captureAttachments ? { data: expect.any(Attachment) } : {}), + }); + expect(audio.data).toBe("AQID"); + } + }, +); + +it.each([ + [false, false], + [false, true], + [true, false], + [true, true], +])( + "applies capture=%s to AI SDK image-edit inputs and masks (URL object: %s)", + async (captureAttachments, maskAsURL) => { + initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + const local = initLogger({ + ...loggerOptions, + setCurrent: false, + captureAttachments, + }); + const data = new URL("data:image/png;base64,AQID"); + const remote = new URL("https://example.com/image.png"); + const manual = new Attachment({ + data: "manual", + filename: "manual.png", + contentType: "image/png", + }); + const prompt = { + images: [ + "AQID", + new Uint8Array([1, 2, 3]), + data, + remote, + remote.href, + manual, + ], + mask: maskAsURL ? data : data.href, + text: "Edit these images", + }; + const decode = vi.spyOn(globalThis, "atob"); + const result = await local.traced(() => + processAISDKGenerateImageInput({ + model: { modelId: "image-model", provider: "test" }, + prompt, + }), + ); + expect(result.input).toHaveProperty( + "prompt", + captureAttachments + ? { + images: [ + expect.any(Attachment), + expect.any(Attachment), + expect.any(Attachment), + remote, + remote.href, + manual, + ], + mask: expect.any(Attachment), + text: prompt.text, + } + : { images: [remote, remote.href, manual], text: prompt.text }, + ); + if (!captureAttachments) expect(decode).not.toHaveBeenCalled(); + expect(prompt.images).toHaveLength(6); + expect(prompt.mask).toBe(maskAsURL ? data : data.href); + }, +); + +it("preserves AI SDK image-edit inputs when conversion fails", () => { + initLogger({ ...loggerOptions, captureAttachments: true }); + const prompt = { + images: ["not valid base64!"], + mask: "not valid base64!", + text: "Edit", + }; + const result = processAISDKGenerateImageInput({ + model: { modelId: "model", provider: "test" }, + prompt, + }); + expect(result.input).toHaveProperty("prompt", prompt); +}); + +it.each([false, true])( + "applies capture=%s to AI SDK content parts containing data URL objects", + (captureAttachments) => { + initLogger({ ...loggerOptions, captureAttachments }); + const data = new URL("data:image/png;base64,AQID"); + const content = [ + { type: "image", image: data }, + { type: "file", data, mediaType: "image/png" }, + { type: "image_url", image_url: { url: data } }, + ]; + const result = processAISDKCallInput({ + model: { modelId: "model", provider: "test" }, + messages: [{ role: "user", content }], + }); + expect(result.input).toHaveProperty("messages", [ + { + role: "user", + content: captureAttachments + ? [ + { type: "image", image: expect.any(Attachment) }, + { + type: "file", + data: expect.any(Attachment), + mediaType: "image/png", + }, + { type: "image_url", image_url: { url: expect.any(Attachment) } }, + ] + : [{ type: "file", mediaType: "image/png" }], + }, + ]); + }, +); + +it("keeps explicit AI SDK attachments with capture disabled", () => { + initLogger({ ...loggerOptions, captureAttachments: false }); + const external = new ExternalAttachment({ + url: "s3://bucket/image.png", + filename: "image.png", + contentType: "image/png", + }); + const content = [ + { type: "image", image: external }, + { type: "file", data: external, mediaType: "image/png" }, + ]; + const { input } = processAISDKCallInput({ + model: { modelId: "model", provider: "test" }, + messages: [{ role: "user", content }], + }); + expect(input).toHaveProperty("messages.0.content", content); +}); + +it.each([true, false])( + "explicit %s overrides an opposing environment setting", + (captureAttachments) => { + vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", String(!captureAttachments)); + const logger = initLogger({ ...loggerOptions, captureAttachments }); + expect(_internalCaptureAttachmentsEnabled(logger)).toBe(captureAttachments); + expect(_internalCaptureAttachmentsEnabled()).toBe(captureAttachments); + initLogger(loggerOptions); + expect(_internalCaptureAttachmentsEnabled()).toBe(captureAttachments); + }, +); + +it("local loggers use the environment, and never inherit or update the global override", () => { + initLogger({ ...loggerOptions, captureAttachments: true }); + const local = initLogger({ ...loggerOptions, setCurrent: false }); + expect(_internalCaptureAttachmentsEnabled(local)).toBe(false); + initLogger({ + ...loggerOptions, + setCurrent: false, + captureAttachments: false, + }); + expect(_internalCaptureAttachmentsEnabled()).toBe(true); + vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", "true"); + initLogger({ ...loggerOptions, captureAttachments: false }); + expect(_internalCaptureAttachmentsEnabled(local)).toBe(true); +}); + +it("isolates SDK state defaults", () => { + const other = new BraintrustState({}); + initLogger({ ...loggerOptions, captureAttachments: true }); + const otherLogger = initLogger({ + ...loggerOptions, + state: other, + captureAttachments: false, + }); + expect(_internalCaptureAttachmentsEnabled(otherLogger)).toBe(false); + expect(other.captureAttachments).toBe(false); + expect(_internalCaptureAttachmentsEnabled()).toBe(true); +}); + +it("keeps concurrent local traces and descendants isolated after global replacement", async () => { + const enabled = initLogger({ + ...loggerOptions, + setCurrent: false, + captureAttachments: true, + }); + const disabled = initLogger({ + ...loggerOptions, + setCurrent: false, + captureAttachments: false, + }); + await Promise.all( + [enabled, disabled].map(async (logger, index) => { + await logger.traced(async (parent) => { + await Promise.resolve(); + initLogger({ ...loggerOptions, captureAttachments: index !== 0 }); + await parent.traced(async () => { + await Promise.resolve(); + const output = processInputAttachments(inlineImage); + if (index === 0) + expect(output.image_url.url).toBeInstanceOf(Attachment); + else expect(output).toBeUndefined(); + }); + }); + }), + ); +}); + +it("does not decode inline media, read local images, or evaluate generated binary getters when disabled", () => { + initLogger({ ...loggerOptions, captureAttachments: false }); + const decode = vi.spyOn(globalThis, "atob"); + const stat = vi.spyOn(iso, "statSync"); + const binary = vi.fn(() => { + throw new Error("must not access media"); + }); + expect( + processAttachmentsInInput([ + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "AQID" }, + }, + ]), + ).toEqual([ + { + type: "image", + source: { type: "base64", media_type: "image/png" }, + }, + ]); + const source = Object.defineProperty( + { type: "base64", media_type: "image/png" }, + "data", + { get: binary, enumerable: true }, + ); + expect(processAttachmentsInInput({ type: "image", source })).toEqual({ + type: "image", + source: { type: "base64", media_type: "image/png" }, + }); + const image = Object.defineProperty({ mediaType: "image/png" }, "base64", { + get: binary, + }); + expect(processAISDKGenerateImageOutput({ images: [image] }, [])).toEqual({}); + expect( + extractOllamaChatInput([ + { + model: "model", + messages: [ + { role: "user", content: "look", images: ["/tmp/private.png"] }, + ], + }, + ]).input, + ).toEqual([ + { + role: "user", + content: "look", + }, + ]); + expect(decode).not.toHaveBeenCalled(); + expect(stat).not.toHaveBeenCalled(); + expect(binary).not.toHaveBeenCalled(); +}); + +it("preserves explicit attachments and remote references with capture disabled", async () => { + const logger = initLogger({ ...loggerOptions, captureAttachments: false }); + const attachment = new Attachment({ + data: new Blob(["explicit"]), + filename: "explicit.txt", + contentType: "text/plain", + }); + logger.log({ input: { attachment } }); + const rows = (await background.drain()) as Array<{ + input?: unknown; + output?: unknown; + metrics?: Record; + }>; + expect( + rows.some( + (row) => + (row.input as { attachment?: unknown })?.attachment === attachment, + ), + ).toBe(true); + const attachments: Attachment[] = []; + const event = { input: { attachment } }; + _exportsForTestingOnly.extractAttachments(event, attachments); + expect(attachments).toEqual([attachment]); + expect( + processInputAttachments({ + ...inlineImage, + image_url: { url: "https://example.com/image.png" }, + }).image_url.url, + ).toBe("https://example.com/image.png"); +}); + +it("preserves a deferred instrumentation policy when a different logger supplies the span parent", async () => { + initLogger({ ...loggerOptions, captureAttachments: true }); + const args = { name: "deferred", [CAPTURE_ATTACHMENTS]: false }; + const deferred = startSpan(args); + const child = deferred.startSpan(); + expect(_internalCaptureAttachmentsEnabled(deferred)).toBe(false); + expect(_internalCaptureAttachmentsEnabled(child)).toBe(false); + child.end(); + deferred.end(); +}); + +it.each([false, true])( + "preserves AI SDK application JSON that resembles media (capture=%s)", + async (captureAttachments) => { + const logger = initLogger({ ...loggerOptions, captureAttachments }); + const object = { type: "file", data: "report text" }; + const toolCall = { + type: "tool-call", + toolCallId: "call", + toolName: "report", + input: object, + args: object, + }; + const toolResult = { + type: "tool-result", + toolCallId: "call", + toolName: "report", + input: object, + output: { type: "json", value: object }, + result: object, + }; + const messages = [ + { role: "assistant", content: [toolCall] }, + { role: "tool", content: [toolResult] }, + ]; + const result = { + object, + output: object, + content: [toolCall, toolResult], + toolCalls: [toolCall], + toolResults: [toolResult], + response: { messages }, + steps: [{ content: [toolCall, toolResult], providerMetadata: object }], + providerMetadata: object, + }; + expect(processAISDKOutput(result, [], captureAttachments)).toEqual(result); + expect( + processAISDKCallInput({ + model: { modelId: "test", provider: "test" }, + messages, + }).input, + ).toHaveProperty("messages", messages); + + const span = logger.startSpan({ name: "structured stream" }); + const streamResult = { + partialObjectStream: (async function* () { + yield object; + })(), + object: Promise.resolve(object), + }; + expect( + patchAISDKStreamingResult({ + defaultDenyOutputPaths: [], + endEvent: {}, + result: streamResult, + span, + startTime: Date.now() / 1000, + }), + ).toBe(true); + for await (const chunk of streamResult.partialObjectStream) + expect(chunk).toEqual(object); + const rows = await background.drain(); + expect(rows.find((row) => "output" in row)).toHaveProperty( + "output.object", + object, + ); + }, +); + +it.each([false, true])( + "captures Agents audio output using the owning policy (%s)", + async (captureAttachments) => { + const logger = initLogger({ + ...loggerOptions, + captureAttachments, + setCurrent: false, + }); + const processor = new OpenAIAgentsTraceProcessor({ logger }); + const trace = { + type: "trace" as const, + traceId: "audio-output", + name: "audio output", + groupId: null, + }; + const audio = { + data: "AQID", + transcript: "Hello", + id: "audio-1", + expires_at: 123, + }; + const output = [{ choices: [{ message: { role: "assistant", audio } }] }]; + const span: OpenAIAgentsSpan = { + type: "trace.span", + traceId: trace.traceId, + spanId: "generation", + parentId: null, + startedAt: null, + endedAt: null, + error: null, + spanData: { type: "generation", output }, + }; + await processor.onTraceStart(trace); + await processor.onSpanStart(span); + initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + await processor.onSpanEnd(span); + const rows = await background.drain(); + const row = rows.find((row) => "output" in row && row.output !== undefined); + expect(row).toHaveProperty("output.0.choices.0.message.audio", { + transcript: "Hello", + id: "audio-1", + expires_at: 123, + ...(captureAttachments ? { data: expect.any(Attachment) } : {}), + }); + expect(audio.data).toBe("AQID"); + const protocolAudio = { + type: "audio", + audio: "AQID", + format: "mp3", + transcript: "Hello", + }; + expect(processInputAttachments(protocolAudio, captureAttachments)).toEqual({ + type: "audio", + format: "mp3", + transcript: "Hello", + ...(captureAttachments ? { audio: expect.any(Attachment) } : {}), + }); + const remoteAudio = { type: "audio", audio: { id: "file-123" } }; + expect(processInputAttachments(remoteAudio, captureAttachments)).toEqual( + remoteAudio, + ); + }, +); + +it.each([false, true])( + "preserves custom Agents JSON without mutating SDK span data (%s)", + async (captureAttachments) => { + const logger = initLogger({ ...loggerOptions, captureAttachments }); + const processor = new OpenAIAgentsTraceProcessor({ logger }); + const trace = { + type: "trace" as const, + traceId: "custom-json", + name: "custom JSON", + groupId: null, + }; + const data = Object.freeze({ + input: Object.freeze({ type: "file", data: "report text" }), + output: Object.freeze({ + type: "image_generation_call", + result: "ordinary result", + }), + }); + const span: OpenAIAgentsSpan = { + type: "trace.span", + traceId: trace.traceId, + spanId: "custom", + parentId: null, + startedAt: null, + endedAt: null, + error: null, + spanData: { type: "custom", name: "report", data }, + }; + await processor.onTraceStart(trace); + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + const rows = await background.drain(); + expect( + rows.find((row) => "output" in row && row.output !== undefined), + ).toMatchObject(data); + expect(span.spanData).toHaveProperty("data", data); + }, +); + +it.each([false, true])( + "applies capture policy to Chat Completions audio (%s)", + async (captureAttachments) => { + initLogger({ ...loggerOptions, captureAttachments }); + const audio = { data: "AQID", transcript: "Hello", id: "audio-1" }; + const result = { + choices: [ + { + index: 0, + message: { role: "assistant" as const, content: null, audio }, + finish_reason: "stop", + }, + ], + }; + await openAIChannels.chatCompletionsCreate.tracePromise( + async () => result, + { + arguments: [{ model: "audio-model", messages: [] }], + }, + ); + const rows = await background.drain(); + expect(rows.find((row) => "output" in row)).toHaveProperty( + "output.0.message.audio", + { + transcript: "Hello", + id: "audio-1", + ...(captureAttachments ? { data: expect.any(Attachment) } : {}), + }, + ); + expect(audio.data).toBe("AQID"); + }, +); + +it.each([false, true])( + "handles raw base64 chat files without changing references (capture=%s)", + async (captureAttachments) => { + initLogger({ ...loggerOptions, captureAttachments }); + const messages = [ + { + role: "user", + content: [ + { + type: "file", + file: { file_data: "AQID", filename: "document.pdf" }, + }, + { type: "file", file: { file_id: "file-123" } }, + { + type: "file", + file: { file_data: "https://example.com/document.pdf" }, + }, + { type: "text", text: "Summarize this document." }, + ], + }, + ]; + await openAIChannels.chatCompletionsCreate.tracePromise( + async () => ({ choices: [] }), + { arguments: [{ model: "model", messages }] }, + ); + const rows = await background.drain(); + expect(rows.find((row) => "input" in row)).toHaveProperty("input", [ + { + role: "user", + content: [ + { + type: "file", + file: { + filename: "document.pdf", + ...(captureAttachments + ? { file_data: expect.any(Attachment) } + : {}), + }, + }, + ...messages[0].content.slice(1), + ], + }, + ]); + expect(messages[0].content[0].file?.file_data).toBe("AQID"); + if (!captureAttachments) expect(JSON.stringify(rows)).not.toContain("AQID"); + }, +); + +it.each([false, true])( + "preserves parsed OpenAI output and tool arguments that resemble media (capture=%s)", + async (captureAttachments) => { + initLogger({ ...loggerOptions, captureAttachments }); + const parsed = { type: "file", data: "report text" }; + const parsedArguments = { + type: "input_image", + image_url: "ordinary input", + }; + const choices = [ + { + index: 0, + message: { + role: "assistant", + content: null, + parsed, + tool_calls: [ + { + type: "function", + id: "call-1", + function: { + name: "report", + arguments: JSON.stringify(parsedArguments), + parsed_arguments: parsedArguments, + }, + }, + ], + }, + }, + ]; + for (const channel of [ + openAIChannels.chatCompletionsCreate, + openAIChannels.betaChatCompletionsParse, + ]) { + await channel.tracePromise(async () => ({ choices }), { + arguments: [{ model: "model", messages: [] }], + }); + const rows = await background.drain(); + expect(rows.find((row) => "output" in row)).toHaveProperty( + "output", + choices, + ); + } + expect(choices[0].message.parsed).toBe(parsed); + expect(choices[0].message.tool_calls[0].function.parsed_arguments).toBe( + parsedArguments, + ); + }, +); + +it.each([ + ["mp3", "audio/mpeg", "mp3"], + ["wav", "audio/wav", "wav"], + ["flac", "audio/flac", "flac"], + ["aac", "audio/aac", "aac"], + ["opus", "audio/ogg", "ogg"], + ["pcm16", "audio/pcm", "pcm"], +] as const)( + "uses the requested %s format for generated chat audio", + async (format, contentType, extension) => { + initLogger({ ...loggerOptions, captureAttachments: true }); + const audio = { data: "AQID", transcript: "Hello", id: "audio-1" }; + const choices = [ + { index: 0, message: { role: "assistant", content: null, audio } }, + ]; + for (const channel of [ + openAIChannels.chatCompletionsCreate, + openAIChannels.betaChatCompletionsParse, + ]) { + await channel.tracePromise(async () => ({ choices }), { + arguments: [ + { + model: "audio-model", + messages: [], + audio: { format, voice: "alloy" }, + }, + ], + }); + const rows = await background.drain(); + expect(rows.find((row) => "output" in row)).toHaveProperty( + "output.0.message.audio", + { + transcript: "Hello", + id: "audio-1", + data: expect.objectContaining({ + reference: expect.objectContaining({ + content_type: contentType, + filename: `audio.${extension}`, + }), + }), + }, + ); + } + expect(audio).toEqual({ data: "AQID", transcript: "Hello", id: "audio-1" }); + }, +); diff --git a/js/src/exports.ts b/js/src/exports.ts index 9abb729d7..2312691ed 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -25,6 +25,7 @@ export { Prompt, ReadonlyAttachment, ReadonlyExperiment, + _internalCaptureAttachmentsEnabled, currentExperiment, currentLogger, currentSpan, diff --git a/js/src/instrumentation/core/channel-tracing.test.ts b/js/src/instrumentation/core/channel-tracing.test.ts index 319d24344..bd8157e31 100644 --- a/js/src/instrumentation/core/channel-tracing.test.ts +++ b/js/src/instrumentation/core/channel-tracing.test.ts @@ -27,6 +27,11 @@ import { traceAsyncChannel, traceStreamingChannel } from "./channel-tracing"; const testChannels = defineChannels( "channel-tracing-test", { + inputContext: channel< + [Record], + { ok: true }, + Record + >({ channelName: "async.input-context", kind: "async" }), asyncBinding: channel<[Record], { ok: true }>({ channelName: "async.binding", kind: "async", @@ -117,6 +122,31 @@ describe("traceAsyncChannel current span binding", () => { }); }); + it("extracts input in the caller's context", async () => { + let inputContextSpan: Span | undefined; + let createdSpan: Span | undefined; + traceAsyncChannel(testChannels.inputContext, { + name: "channel-tracing-test", + type: "function", + extractInput: (_args, _event, span) => { + inputContextSpan = currentSpan(); + createdSpan = span; + return { input: "input", metadata: undefined }; + }, + extractOutput: (result) => result, + extractMetrics: () => ({}), + }); + + await testChannels.inputContext.tracePromise( + async () => ({ ok: true as const }), + { arguments: [{}] } as any, + ); + + // AI SDK child tracing compares the caller's span with the created span. + expect(createdSpan).toBeDefined(); + expect(inputContextSpan).not.toBe(createdSpan); + }); + it("limits channel provenance to directly instrumented spans", async () => { traceAsyncChannel(testChannels.provenance, { name: "channel-parent", diff --git a/js/src/instrumentation/core/channel-tracing.ts b/js/src/instrumentation/core/channel-tracing.ts index 0ab5ad805..0416d4d33 100644 --- a/js/src/instrumentation/core/channel-tracing.ts +++ b/js/src/instrumentation/core/channel-tracing.ts @@ -1,6 +1,6 @@ import { debugLogger } from "../../debug-logger"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; -import { _internalGetGlobalState, startSpan } from "../../logger"; +import { _internalGetGlobalState, startSpan, withCurrent } from "../../logger"; import type { Span } from "../../logger"; import { withSpanInstrumentationName, @@ -468,9 +468,8 @@ export function traceAsyncChannel( const { span, startTime } = spanData; try { - const output = config.extractOutput( - asyncEndEvent.result, - asyncEndEvent, + const output = withCurrent(span, () => + config.extractOutput(asyncEndEvent.result, asyncEndEvent), ); const metrics = config.extractMetrics( asyncEndEvent.result, @@ -595,20 +594,25 @@ export function traceStreamingChannel( let error: string | undefined; if (config.aggregateChunks) { - const aggregated = config.aggregateChunks( - chunks, - asyncEndEvent.result, - asyncEndEvent, - startTime, + const aggregateChunks = config.aggregateChunks; + const aggregated = withCurrent(span, () => + aggregateChunks( + chunks, + asyncEndEvent.result, + asyncEndEvent, + startTime, + ), ); output = aggregated.output; metrics = aggregated.metrics; metadata = aggregated.metadata; error = aggregated.error; } else { - output = config.extractOutput( - chunks as unknown as StreamingResult, - asyncEndEvent, + output = withCurrent(span, () => + config.extractOutput( + chunks as unknown as StreamingResult, + asyncEndEvent, + ), ); metrics = config.extractMetrics( chunks as unknown as StreamingResult, @@ -705,9 +709,11 @@ export function traceStreamingChannel( } | undefined; try { - const output = config.extractOutput( - asyncEndEvent.result as StreamingResult, - asyncEndEvent, + const output = withCurrent(span, () => + config.extractOutput( + asyncEndEvent.result as StreamingResult, + asyncEndEvent, + ), ); const metrics = config.extractMetrics( asyncEndEvent.result as StreamingResult, diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts index c37257b8e..144bb11c0 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts @@ -15,6 +15,9 @@ const telemetryMocks = vi.hoisted(() => ({ vi.mock("../../isomorph", () => ({ default: { newTracingChannel: vi.fn(), + getEnv: vi.fn((name: string) => + name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, + ), }, })); diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.ts index d96042db6..1566e8f73 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.ts @@ -15,9 +15,13 @@ import { } from "../../../util/index"; import { getCurrentUnixTimestamp } from "../../util"; import { + _internalCaptureAttachmentsEnabled, _internalStartSpanWithInitialMerge, Attachment, + BaseAttachment, + CAPTURE_ATTACHMENTS, currentSpan, + deepCopyEvent, startSpan, type Span, withCurrent, @@ -29,6 +33,8 @@ import { import { convertDataToBlob, getExtensionFromMediaType, + omitMediaData, + processInputAttachments, } from "../../wrappers/attachment-utils"; import { normalizeAISDKLoggedOutput } from "../../wrappers/ai-sdk/normalize-logged-output"; import { serializeAISDKToolsForLogging } from "../../wrappers/ai-sdk/tool-serialization"; @@ -805,6 +811,9 @@ function subscribeToHarnessContinuation( result: AISDKResult | AsyncIterable; }; const span = { + [CAPTURE_ATTACHMENTS]: _internalCaptureAttachmentsEnabled( + typeof parent === "string" ? undefined : parent, + ), end: () => endHarnessTurn(parent), log: (update: Parameters[0]) => updateHarnessTurn( @@ -867,6 +876,7 @@ function subscribeToHarnessContinuation( output: processAISDKOutput( endEvent.result, resolveDenyOutputPaths(endEvent, defaultDenyOutputPaths), + _internalCaptureAttachmentsEnabled(span), ), }); span.end(); @@ -1457,7 +1467,9 @@ const processMessage = (message: any): any => { if (Array.isArray(message.content)) { return { ...message, - content: message.content.map(processContentPart), + content: message.content + .map(processContentPart) + .filter((part: unknown) => part !== undefined), }; } @@ -1473,14 +1485,18 @@ const processMessage = (message: any): any => { const processPromptContent = (prompt: any): any => { if (Array.isArray(prompt)) { - return prompt.map(processContentPart); + return prompt + .map(processContentPart) + .filter((part: unknown) => part !== undefined); } if (prompt.content) { if (Array.isArray(prompt.content)) { return { ...prompt, - content: prompt.content.map(processContentPart), + content: prompt.content + .map(processContentPart) + .filter((part: unknown) => part !== undefined), }; } else if (typeof prompt.content === "object") { return { @@ -1497,7 +1513,12 @@ const processContentPart = (part: any): any => { if (!part || typeof part !== "object") return part; try { + if (part.type === "tool-result") return processInputAttachments(part); + const captureAttachments = _internalCaptureAttachmentsEnabled(); + if (part.type === "image" && part.image) { + if (!captureAttachments) + return isInlineMedia(part.image) ? omitMediaData(part, "image") : part; const imageAttachment = convertImageToAttachment( part.image, part.mimeType || part.mediaType, @@ -1510,11 +1531,9 @@ const processContentPart = (part: any): any => { } } - if ( - part.type === "file" && - part.data && - (part.mimeType || part.mediaType) - ) { + if (part.type === "file" && part.data) { + if (!captureAttachments) + return isInlineMedia(part.data) ? omitMediaData(part, "data") : part; const fileAttachment = convertDataToAttachment( part.data, part.mimeType || part.mediaType, @@ -1530,6 +1549,13 @@ const processContentPart = (part: any): any => { if (part.type === "image_url" && part.image_url) { if (typeof part.image_url === "object" && part.image_url.url) { + if (!captureAttachments) + return isInlineMedia(part.image_url.url) + ? omitMediaData({ + ...part, + image_url: omitMediaData(part.image_url, "url"), + }) + : part; const imageAttachment = convertImageToAttachment(part.image_url.url); if (imageAttachment) { return { @@ -1550,11 +1576,21 @@ const processContentPart = (part: any): any => { return part; }; +// Explicit attachments and remote URLs are references, not inline media. +const isInlineMedia = (value: unknown): boolean => + !(value instanceof BaseAttachment) && + !(value instanceof URL && value.protocol !== "data:") && + !(typeof value === "string" && /^https?:/i.test(value)); + const convertImageToAttachment = ( image: any, explicitMimeType?: string, ): Attachment | null => { try { + if (image instanceof URL) { + if (image.protocol !== "data:") return null; + image = image.href; + } if (typeof image === "string" && image.startsWith("data:")) { const [mimeTypeSection, base64Data] = image.split(","); const mimeType = mimeTypeSection.match(/data:(.*?);/)?.[1]; @@ -1608,6 +1644,10 @@ const convertDataToAttachment = ( if (!mimeType) return null; try { + if (data instanceof URL) { + if (data.protocol !== "data:") return null; + data = data.href; + } let blob: Blob | null = null; if (typeof data === "string" && data.startsWith("data:")) { @@ -1658,14 +1698,19 @@ export function processAISDKGenerateImageInput( } const processedPrompt = { ...prompt }; + const captureAttachments = _internalCaptureAttachmentsEnabled(); if (Array.isArray(prompt.images)) { - processedPrompt.images = prompt.images.map( - (image) => convertImageToAttachment(image, "image/png") ?? image, - ); + processedPrompt.images = captureAttachments + ? prompt.images.map( + (image) => convertImageToAttachment(image, "image/png") ?? image, + ) + : prompt.images.filter((image) => !isInlineMedia(image)); } if (prompt.mask !== undefined) { - processedPrompt.mask = - convertImageToAttachment(prompt.mask, "image/png") ?? prompt.mask; + if (captureAttachments) + processedPrompt.mask = + convertImageToAttachment(prompt.mask, "image/png") ?? prompt.mask; + else if (isInlineMedia(prompt.mask)) delete processedPrompt.mask; } return processAISDKCallInput({ @@ -2109,7 +2154,11 @@ function sanitizeAISDKCallInputValue(value: unknown, depth = 0): unknown { return undefined; } - if (value === null || typeof value !== "object") { + if ( + value === null || + typeof value !== "object" || + value instanceof BaseAttachment + ) { return value; } @@ -2206,7 +2255,11 @@ function sanitizeAISDKMetadataValue(value: unknown, depth = 0): unknown { return "[Function]"; } - if (value === null || typeof value !== "object") { + if ( + value === null || + typeof value !== "object" || + value instanceof BaseAttachment + ) { return value; } @@ -2696,6 +2749,7 @@ function prepareAISDKChildTracing( output: processAISDKOutput( output as AISDKResult, activeEntry.denyOutputPaths, + _internalCaptureAttachmentsEnabled(span), ), metrics, ...mergeMetadataPayload(metadataPayload, missingUsageMetadata), @@ -3184,6 +3238,7 @@ export function patchAISDKStreamingResult(args: { } const resultRecord = result as Record; + const captureAttachments = _internalCaptureAttachmentsEnabled(span); attachKnownResultPromiseHandlers(resultRecord); let finalized = false; const finalize = ( @@ -3266,6 +3321,7 @@ export function patchAISDKStreamingResult(args: { const processedOutput = await processAISDKStreamingOutput( result, resolveDenyOutputPaths(endEvent, defaultDenyOutputPaths), + captureAttachments, ); const output = transformOutput ? transformOutput(processedOutput) @@ -3621,8 +3677,13 @@ function createPatchedAsyncIterable( async function processAISDKStreamingOutput( result: AISDKResult, denyOutputPaths: string[], + captureAttachments: boolean, ): Promise | AISDKResult> { - const output = processAISDKOutput(result, denyOutputPaths); + const output = processAISDKOutput( + result, + denyOutputPaths, + captureAttachments, + ); if (!output || typeof output !== "object") { return output; @@ -3817,14 +3878,48 @@ function isAsyncGenerator(value: unknown): value is AsyncGenerator { export function processAISDKOutput( output: AISDKResult, denyOutputPaths: string[], + captureAttachments = _internalCaptureAttachmentsEnabled(), ): Record | AISDKResult { if (!output) return output; const merged = extractSerializableOutputFields(output); + // Only provider content containers contain media. Structured outputs, tool + // arguments, and metadata can use the same field names for ordinary JSON. + const processMediaContainers = ( + container: Record, + ): Record => + Object.fromEntries( + Object.entries(container).map(([key, value]) => { + if ( + ["content", "messages", "responseMessages", "toolResults"].includes( + key, + ) && + Array.isArray(value) + ) { + return [key, processInputAttachments(value, captureAttachments)]; + } + if (key === "steps" && Array.isArray(value)) { + return [ + key, + value.map((step) => + isObject(step) ? processMediaContainers(step) : step, + ), + ]; + } + if (key === "response" && isObject(value)) { + return [key, processMediaContainers(value)]; + } + return [key, value]; + }), + ); const deleteOutputPaths = denyOutputPaths.filter((path) => path.toLowerCase().endsWith("headers"), ); - const sanitized = omit(merged, denyOutputPaths, deleteOutputPaths); + const sanitized = omit( + processMediaContainers(merged), + denyOutputPaths, + deleteOutputPaths, + ); // Transport payloads can contain nested provider request/response headers; keep // user/model/tool payload fields named "headers" outside these roots intact. @@ -3917,9 +4012,10 @@ export function processAISDKGenerateImageOutput( omit(summarized, denyOutputPaths), ) as Record; if (generatedFiles.length > 0) { - loggedOutput.images = generatedFiles.map((file, index) => - convertAISDKGeneratedFileToAttachment(file, index), - ); + const images = generatedFiles + .map((file, index) => convertAISDKGeneratedFileToAttachment(file, index)) + .filter((image) => image !== undefined); + if (images.length) loggedOutput.images = images; } return loggedOutput; @@ -3933,6 +4029,8 @@ function convertAISDKGeneratedFileToAttachment( return file; } + if (!_internalCaptureAttachmentsEnabled()) return undefined; + const generatedFile = file as AISDKGeneratedFile & Record; const generatedMediaType = safeSerializableFieldRead( generatedFile, @@ -4300,13 +4398,6 @@ function extractGatewayRoutingInfo(result: AISDKResult): { return null; } -/** - * Deep copy an object via JSON serialization. - */ -function deepCopy(obj: Record): Record { - return JSON.parse(JSON.stringify(obj)); -} - /** * Parse a JSON path string into an array of keys. */ @@ -4406,7 +4497,7 @@ function omit( paths: string[], deletePaths: string[] = [], ): Record { - const result = deepCopy(obj); + const result = deepCopyEvent({ output: obj }).output; const deletePathSet = new Set(deletePaths); for (const path of paths) { diff --git a/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts b/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts index 3805b2dd3..6c6bbffd7 100644 --- a/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { + Attachment, _exportsForTestingOnly, currentSpan, initLogger, @@ -41,6 +42,61 @@ describe("braintrustAISDKTelemetry", () => { _exportsForTestingOnly.clearTestBackgroundLogger(); }); + it.each([false, true])( + "keeps the owning media policy in delayed telemetry callbacks (%s)", + async (captureAttachments) => { + initLogger({ + projectName: "tmp-luca-telemetry-capture", + projectId: "test-project-id", + captureAttachments, + }); + const telemetry = braintrustAISDKTelemetry(); + const callId = "delayed-media"; + telemetry.onStart({ callId, operationId: "ai.generateText" }); + initLogger({ + projectName: "tmp-luca-telemetry-capture", + projectId: "test-project-id", + captureAttachments: !captureAttachments, + }); + const messages = [ + { + role: "user", + content: [{ type: "image", image: "data:image/png;base64,AQID" }], + }, + ]; + telemetry.onLanguageModelCallStart({ callId, messages }); + const content = [{ type: "file", mediaType: "image/png", data: "AQID" }]; + telemetry.onLanguageModelCallEnd({ callId, content }); + telemetry.onEnd({ callId, operationId: "ai.generateText", content }); + const rows = await backgroundLogger.drain(); + const outputs = rows.filter( + (row) => "output" in row && row.output !== undefined, + ); + expect(outputs).toHaveLength(2); + for (const row of outputs) + expect(row).toHaveProperty("output.content", [ + { + type: "file", + mediaType: "image/png", + ...(captureAttachments ? { data: expect.any(Attachment) } : {}), + }, + ]); + const inputRow = rows.find( + (row) => + "input" in row && + row.input && + typeof row.input === "object" && + "messages" in row.input, + ); + expect(inputRow).toHaveProperty( + "input.messages.0.content", + captureAttachments + ? [{ type: "image", image: expect.any(Attachment) }] + : [], + ); + }, + ); + it.each(["generateText", "streamText"])( "captures provider tools in %s alongside local tools", async (operation) => { diff --git a/js/src/instrumentation/providers/anthropic-instrumentation.test.ts b/js/src/instrumentation/providers/anthropic-instrumentation.test.ts index 93872d308..c945c1f2e 100644 --- a/js/src/instrumentation/providers/anthropic-instrumentation.test.ts +++ b/js/src/instrumentation/providers/anthropic-instrumentation.test.ts @@ -4,6 +4,9 @@ import { describe, it, expect, vi } from "vitest"; vi.mock("../../isomorph", () => ({ default: { newTracingChannel: vi.fn(), + getEnv: vi.fn((name: string) => + name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, + ), }, })); @@ -21,22 +24,28 @@ const aggregateAnthropicStreamChunksForTest = (chunks: unknown[]) => aggregateAnthropicStreamChunks(chunks as any); // Mock startSpan from logger -vi.mock("../../logger", () => ({ - startSpan: vi.fn(() => ({ - log: vi.fn(), - end: vi.fn(), - })), - _internalGetGlobalState: vi.fn(() => undefined), - Attachment: class Attachment { - reference: any; - constructor(opts: any) { - this.reference = { - filename: opts.filename, - content_type: opts.contentType, - }; - } - }, -})); +vi.mock("../../logger", async () => { + const { default: iso } = await import("../../isomorph"); + return { + _internalCaptureAttachmentsEnabled: () => + iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", + withCurrent: (_span: unknown, callback: () => unknown) => callback(), + startSpan: vi.fn(() => ({ + log: vi.fn(), + end: vi.fn(), + })), + _internalGetGlobalState: vi.fn(() => undefined), + Attachment: class Attachment { + reference: any; + constructor(opts: any) { + this.reference = { + filename: opts.filename, + content_type: opts.contentType, + }; + } + }, + }; +}); describe("parseMetricsFromUsage", () => { it("should return empty object for null usage", () => { diff --git a/js/src/instrumentation/providers/anthropic-instrumentation.ts b/js/src/instrumentation/providers/anthropic-instrumentation.ts index e02add721..0b5f3f112 100644 --- a/js/src/instrumentation/providers/anthropic-instrumentation.ts +++ b/js/src/instrumentation/providers/anthropic-instrumentation.ts @@ -1,7 +1,9 @@ +import { omitMediaData } from "../../wrappers/attachment-utils"; import { toLoggedError } from "../core"; import { traceStreamingChannel } from "../core/channel-tracing"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import { + _internalCaptureAttachmentsEnabled, Attachment, startSpan as startBaseSpan, withCurrent, @@ -1639,15 +1641,25 @@ function convertBase64ToAttachment( /** * Process input to convert base64 attachments (images, PDFs, etc.) to Attachment objects. */ -export function processAttachmentsInInput(input: unknown): unknown { +export function processAttachmentsInInput( + input: unknown, + captureAttachments = _internalCaptureAttachmentsEnabled(), +): unknown { if (Array.isArray(input)) { - return input.map(processAttachmentsInInput); + return input + .map((value) => processAttachmentsInInput(value, captureAttachments)) + .filter((value) => value !== undefined); } if (isObject(input)) { // Check for Anthropic's content blocks with base64 data // Supports both "image" and "document" types (for PDFs, etc.) if (isAnthropicBase64ContentBlock(input)) { + if (!captureAttachments) + return omitMediaData({ + ...input, + source: omitMediaData(input.source, "data"), + }); return { ...input, source: convertBase64ToAttachment(input.source, input.type), @@ -1657,7 +1669,8 @@ export function processAttachmentsInInput(input: unknown): unknown { // Recursively process nested objects const processed: Record = {}; for (const [key, value] of Object.entries(input)) { - processed[key] = processAttachmentsInInput(value); + const result = processAttachmentsInInput(value, captureAttachments); + if (result !== undefined) processed[key] = result; } return processed; } diff --git a/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts b/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts index 0334d7638..1e525ab09 100644 --- a/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts +++ b/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts @@ -21,6 +21,7 @@ describe("ElevenLabs instrumentation", () => { beforeEach(() => { logger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ + captureAttachments: true, projectName: "tmp-luca-elevenlabs-unit", projectId: "test-project-id", }); @@ -29,44 +30,56 @@ describe("ElevenLabs instrumentation", () => { _exportsForTestingOnly.clearTestBackgroundLogger(); }); - it("keeps promise helpers and stream identity, and copies bytes before the caller can mutate them", async () => { - const bytes = new Uint8Array([0, 1, 2, 3]); - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(bytes.subarray(1, 3)); - controller.close(); - }, - }); - const promise = Object.assign(Promise.resolve(stream), { - withRawResponse: () => - Promise.resolve({ - data: stream, - rawResponse: { - headers: new Headers({ "content-type": "audio/mpeg" }), - }, - }), - }); - const result = elevenLabsChannels.convert.invoke( - () => promise, - undefined, - ["voice", request], - {}, - ); - expect(result).toBe(promise); - expect((await result.withRawResponse()).data).toBe(stream); - const reader = stream.getReader(); - expect((await reader.read()).value).toEqual(new Uint8Array([1, 2])); - bytes.fill(9); - await reader.read(); - const spans = (await logger.drain()) as Record[]; - const output = spans.find((row: any) => row.output)?.output as any; - const attachment = output.content[0].file.file_data as Attachment; - expect(attachment).toBeInstanceOf(Attachment); - expect(output.content[0].file.byte_size).toBe(2); - expect( - new Uint8Array(await (await attachment.data()).arrayBuffer()), - ).toEqual(new Uint8Array([1, 2])); - }); + it.each([false, true])( + "keeps promise helpers and stream identity with capture=%s", + async (captureAttachments) => { + initLogger({ + projectName: "tmp-luca-elevenlabs-unit", + projectId: "test-project-id", + captureAttachments, + }); + const bytes = new Uint8Array([0, 1, 2, 3]); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(bytes.subarray(1, 3)); + controller.close(); + }, + }); + const promise = Object.assign(Promise.resolve(stream), { + withRawResponse: () => + Promise.resolve({ + data: stream, + rawResponse: { + headers: new Headers({ "content-type": "audio/mpeg" }), + }, + }), + }); + const result = elevenLabsChannels.convert.invoke( + () => promise, + undefined, + ["voice", request], + {}, + ); + expect(result).toBe(promise); + expect((await result.withRawResponse()).data).toBe(stream); + const reader = stream.getReader(); + expect((await reader.read()).value).toEqual(new Uint8Array([1, 2])); + bytes.fill(9); + await reader.read(); + const spans = (await logger.drain()) as Record[]; + const output = spans.find((row: any) => row.output)?.output as any; + if (!captureAttachments) { + expect(output.content).toEqual([]); + return; + } + const attachment = output.content[0].file.file_data as Attachment; + expect(attachment).toBeInstanceOf(Attachment); + expect(output.content[0].file.byte_size).toBe(2); + expect( + new Uint8Array(await (await attachment.data()).arrayBuffer()), + ).toEqual(new Uint8Array([1, 2])); + }, + ); it.each(["cancel", "error", "break"])( "omits partial audio on %s", diff --git a/js/src/instrumentation/providers/elevenlabs-instrumentation.ts b/js/src/instrumentation/providers/elevenlabs-instrumentation.ts index 4a0ac6774..d6a5a2941 100644 --- a/js/src/instrumentation/providers/elevenlabs-instrumentation.ts +++ b/js/src/instrumentation/providers/elevenlabs-instrumentation.ts @@ -1,6 +1,12 @@ import { isObject, SpanTypeAttribute } from "../../../util"; import { debugLogger } from "../../debug-logger"; -import { Attachment, startSpan, withCurrent, type Span } from "../../logger"; +import { + _internalCaptureAttachmentsEnabled, + Attachment, + startSpan, + withCurrent, + type Span, +} from "../../logger"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -70,8 +76,9 @@ export function registerElevenLabsInstrumentation(): void { file instanceof Blob ? file.type || "application/octet-stream" : "application/octet-stream"; - const blob = - file instanceof Blob + const blob = !_internalCaptureAttachmentsEnabled() + ? undefined + : file instanceof Blob ? file : file instanceof Uint8Array ? new Blob([new Uint8Array(file)], { type: contentType }) @@ -121,7 +128,8 @@ export function registerElevenLabsInstrumentation(): void { }, ([request], span) => { const file = request.file; - if (!isAsyncIterable(file)) return; + if (!_internalCaptureAttachmentsEnabled(span) || !isAsyncIterable(file)) + return; const chunks: Uint8Array[] = []; const filename = isObject(file) && typeof file.path === "string" @@ -283,6 +291,7 @@ function captureSpeech( started: number, headers?: Headers, ): void { + const captureAttachments = _internalCaptureAttachmentsEnabled(span); const format = request.outputFormat ?? "mp3_44100_128"; const formatType = format.startsWith("mp3_") ? "audio/mpeg" @@ -320,8 +329,10 @@ function captureSpeech( }); } first = false; - chunks.push(new Uint8Array(chunk)); - bytes += chunk.byteLength; + if (captureAttachments) { + chunks.push(new Uint8Array(chunk)); + bytes += chunk.byteLength; + } }; const complete = () => { if (stopped) return; @@ -365,8 +376,17 @@ function captureSpeech( finish(error); }; const timestampChunk = (chunk: ElevenLabsTimestampAudio) => { - const binary = atob(chunk.audioBase64); - observe(Uint8Array.from(binary, (character) => character.charCodeAt(0))); + if (captureAttachments) { + const binary = atob(chunk.audioBase64); + observe(Uint8Array.from(binary, (character) => character.charCodeAt(0))); + } else if (first && chunk.audioBase64) { + if (method.startsWith("stream")) { + span.log({ + metrics: { time_to_first_token: Date.now() / 1000 - started }, + }); + } + first = false; + } if (chunk.alignment || chunk.normalizedAlignment) alignments.push({ alignment: chunk.alignment, diff --git a/js/src/instrumentation/providers/eve-instrumentation.test.ts b/js/src/instrumentation/providers/eve-instrumentation.test.ts index f5b77d166..3a2b743a4 100644 --- a/js/src/instrumentation/providers/eve-instrumentation.test.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.test.ts @@ -62,11 +62,13 @@ describe("braintrustEveInstrumentation", () => { initLogger({ projectName: "eve-instrumentation.test.ts", projectId: "test-project-id", + captureAttachments: false, }); }); afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllEnvs(); _exportsForTestingOnly.clearTestBackgroundLogger(); }); @@ -771,130 +773,139 @@ describe("braintrustEveInstrumentation", () => { expect(step?.root_span_id).toBe(parent?.root_span_id); }); - it("resumes spans and turn metrics from Eve operation state", async () => { - const firstProvider = braintrustEveInstrumentation(); - const turnContext = providerContext(); - const modelContext = providerContext(); - const scope = ATTEMPT_SCOPE; + it.each([false, true])( + "resumes spans and turn metrics from Eve operation state (legacy: %s)", + async (legacy) => { + const firstProvider = braintrustEveInstrumentation(); + const turnContext = providerContext(); + const modelContext = providerContext(); + const scope = ATTEMPT_SCOPE; - await firstProvider.events["turn.started"]( - { - idempotencyKey: "turn:session-root:turn-0", - rootSessionId: "session-root", - sequence: 0, - sessionId: "session-root", - turnId: "turn-0", - type: "turn.started", - }, - turnContext, - ); - await firstProvider.events["model.call.started"]( - { - idempotencyKey: "model:attempt-0:0", - input: { - messages: [{ content: "Resume this call", role: "user" }], + await firstProvider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", }, - model: { - modelId: "gpt-5.4-mini", - provider: "openai.responses", + turnContext, + ); + await firstProvider.events["model.call.started"]( + { + idempotencyKey: "model:attempt-0:0", + input: { + messages: [{ content: "Resume this call", role: "user" }], + }, + model: { + modelId: "gpt-5.4-mini", + provider: "openai.responses", + }, + scope, + type: "model.call.started", }, - scope, - type: "model.call.started", - }, - modelContext, - ); + modelContext, + ); - const persisted = modelContext.state.get(); - expect(persisted).toMatchObject({ - exported: expect.any(String), - rootSpanId: expect.any(String), - spanId: expect.any(String), - }); + const persisted = { + ...(modelContext.state.get() as Record), + }; + if (legacy) delete persisted.captureAttachments; + expect(persisted).toMatchObject({ + exported: expect.any(String), + rootSpanId: expect.any(String), + spanId: expect.any(String), + }); - const resumedProvider = braintrustEveInstrumentation(); - await resumedProvider.events["model.call.completed"]( - { - content: [{ text: "Resumed output", type: "text" }], - finishReason: "stop", - idempotencyKey: "model:attempt-0:0", - scope, - type: "model.call.completed", - usage: { inputTokens: 4, outputTokens: 2 }, - }, - providerContext(persisted), - ); - const secondModelContext = providerContext(); - await resumedProvider.events["model.call.started"]( - { - idempotencyKey: "model:attempt-0:1", - input: { - messages: [{ content: "Continue this call", role: "user" }], + const resumedProvider = braintrustEveInstrumentation(); + await resumedProvider.events["model.call.completed"]( + { + content: [{ text: "Resumed output", type: "text" }], + finishReason: "stop", + idempotencyKey: "model:attempt-0:0", + scope, + type: "model.call.completed", + usage: { inputTokens: 4, outputTokens: 2 }, }, - model: { - modelId: "gpt-5.4-mini", - provider: "openai.responses", + providerContext(persisted), + ); + const secondModelContext = providerContext(); + await resumedProvider.events["model.call.started"]( + { + idempotencyKey: "model:attempt-0:1", + input: { + messages: [{ content: "Continue this call", role: "user" }], + }, + model: { + modelId: "gpt-5.4-mini", + provider: "openai.responses", + }, + scope: { ...scope, stepIndex: 1 }, + type: "model.call.started", }, - scope: { ...scope, stepIndex: 1 }, - type: "model.call.started", - }, - secondModelContext, - ); - const secondPersisted = secondModelContext.state.get(); - expect(secondPersisted).toMatchObject({ - turnMetricContributions: { - "model:attempt-0:0": { + secondModelContext, + ); + const secondPersisted = { + ...(secondModelContext.state.get() as Record), + }; + if (legacy) delete secondPersisted.captureAttachments; + expect(secondPersisted).toMatchObject({ + turnMetricContributions: { + "model:attempt-0:0": { + completion_tokens: 2, + prompt_tokens: 4, + tokens: 6, + }, + }, + }); + const finalProvider = braintrustEveInstrumentation(); + await finalProvider.events["model.call.completed"]( + { + content: [{ text: "Final resumed output", type: "text" }], + finishReason: "stop", + idempotencyKey: "model:attempt-0:1", + scope: { ...scope, stepIndex: 1 }, + type: "model.call.completed", + usage: { inputTokens: 3, outputTokens: 1 }, + }, + providerContext(secondPersisted), + ); + + const spans = (await backgroundLogger.drain()) as Array< + Record + >; + const steps = spans.filter( + (span) => span.span_attributes?.name === "eve.step", + ); + expect(steps).toHaveLength(2); + expect(steps[0]).toMatchObject({ + input: [{ content: "Resume this call", role: "user" }], + metrics: { completion_tokens: 2, prompt_tokens: 4, tokens: 6, }, - }, - }); - const finalProvider = braintrustEveInstrumentation(); - await finalProvider.events["model.call.completed"]( - { - content: [{ text: "Final resumed output", type: "text" }], - finishReason: "stop", - idempotencyKey: "model:attempt-0:1", - scope: { ...scope, stepIndex: 1 }, - type: "model.call.completed", - usage: { inputTokens: 3, outputTokens: 1 }, - }, - providerContext(secondPersisted), - ); - - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - const steps = spans.filter( - (span) => span.span_attributes?.name === "eve.step", - ); - expect(steps).toHaveLength(2); - expect(steps[0]).toMatchObject({ - input: [{ content: "Resume this call", role: "user" }], - metrics: { - completion_tokens: 2, - prompt_tokens: 4, - tokens: 6, - }, - output: [ - { - finish_reason: "stop", - message: { - content: "Resumed output", - role: "assistant", + output: [ + { + finish_reason: "stop", + message: { + content: "Resumed output", + role: "assistant", + }, }, - }, - ], - }); - const turn = spans.find( - (span) => span.span_attributes?.name === "eve.turn", - ); - expect(turn?.metrics).toMatchObject({ - completion_tokens: 3, - prompt_tokens: 7, - tokens: 10, - }); - }); + ], + }); + const turn = spans.find( + (span) => span.span_attributes?.name === "eve.turn", + ); + expect(turn?.metrics).toMatchObject({ + completion_tokens: 3, + prompt_tokens: 7, + tokens: 10, + }); + }, + ); it("does not double-count replayed model completions", async () => { const provider = braintrustEveInstrumentation(); @@ -1120,159 +1131,217 @@ describe("braintrustEveInstrumentation", () => { expect(JSON.stringify(step?.input)).not.toContain("secret"); }); - it("converts inline model and tool-result media to attachments", async () => { - const provider = braintrustEveInstrumentation(); - const actionContext = providerContext(); - await provider.events["turn.started"]( - { - idempotencyKey: "turn:session-root:turn-0", - rootSessionId: "session-root", - sequence: 0, - sessionId: "session-root", - turnId: "turn-0", - type: "turn.started", - }, - providerContext(), - ); - await provider.events["model.call.started"]( - { - idempotencyKey: "model:attempt-0:0", - input: { - messages: [ - { - content: [ - { - data: "AQID", - filename: "tiny.png", - mediaType: "image/png", - type: "file", - }, - { - image: "DQ4P", - mediaType: "image/png", - type: "image", - }, + it.each( + [ + { option: false, env: "true", enabled: false }, + { option: true, env: "false", enabled: true }, + { option: undefined, env: undefined, enabled: false }, + { option: undefined, env: "true", enabled: true }, + ].flatMap((policy) => [ + { ...policy, legacy: false }, + { ...policy, legacy: true }, + ]), + )( + "applies the persisted or current policy to model media and preserves action JSON ($option, $env, legacy: $legacy)", + async ({ option, env, enabled, legacy }) => { + const captureAttachments = legacy ? !enabled : enabled; + vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", env); + const logger = initLogger({ + projectName: "eve-instrumentation.test.ts", + projectId: "test-project-id", + setCurrent: false, + captureAttachments: option, + }); + const provider = braintrustEveInstrumentation(); + const actionContext = providerContext(); + const turnContext = providerContext(); + await logger.traced(() => + provider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", + }, + turnContext, + ), + ); + initLogger({ + projectName: "eve-instrumentation.test.ts", + projectId: "test-project-id", + captureAttachments: !enabled, + }); + const persistedTurn = { + ...(turnContext.state.get() as Record), + }; + if (legacy) delete persistedTurn.captureAttachments; + // Recreate the bridge from persisted state, as Eve does across workers. + const resumedProvider = braintrustEveInstrumentation(); + await resumedProvider.events["turn.started"]( + { + idempotencyKey: "turn:session-root:turn-0", + rootSessionId: "session-root", + sequence: 0, + sessionId: "session-root", + turnId: "turn-0", + type: "turn.started", + }, + providerContext(persistedTurn), + ); + const decode = vi.spyOn(globalThis, "atob"); + await resumedProvider.events["model.call.started"]( + { + idempotencyKey: "model:attempt-0:0", + input: { + messages: [ + { + content: [ + { + data: "AQID", + filename: "tiny.png", + mediaType: "image/png", + type: "file", + }, + { + image: "DQ4P", + mediaType: "image/png", + type: "image", + }, + { + data: "not valid base64!", + filename: "invalid.png", + mediaType: "image/png", + type: "file", + }, + { type: "image", image: "https://example.com/image.png" }, + ], + role: "user", + }, + { + content: [ + { + output: { + type: "content", + value: [ + { + data: "BAUG", + mediaType: "image/png", + type: "image-data", + }, + ], + }, + toolCallId: "call-image", + toolName: "render", + type: "tool-result", + }, + ], + role: "tool", + }, + ], + }, + model: { modelId: "gpt-5.4-mini", provider: "openai.responses" }, + scope: ATTEMPT_SCOPE, + type: "model.call.started", + }, + providerContext(), + ); + await provider.events["action.started"]( + { + callId: "call-render", + idempotencyKey: "action:session-root:turn-0:call-render", + input: { + data: "report text", + type: "file", + }, + name: "render", + scope: ATTEMPT_SCOPE, + type: "action.started", + }, + actionContext, + ); + await provider.events["action.completed"]( + { + idempotencyKey: "action:session-root:turn-0:call-render", + output: { + output: { + type: "content", + value: [ { - data: "not valid base64!", - filename: "invalid.png", + data: "ordinary JSON data", mediaType: "image/png", - type: "file", - }, - ], - role: "user", - }, - { - content: [ - { - output: { - type: "content", - value: [ - { - data: "BAUG", - mediaType: "image/png", - type: "image-data", - }, - ], - }, - toolCallId: "call-image", - toolName: "render", - type: "tool-result", + type: "image-data", }, ], - role: "tool", }, - ], - }, - model: { modelId: "gpt-5.4-mini", provider: "openai.responses" }, - scope: ATTEMPT_SCOPE, - type: "model.call.started", - }, - providerContext(), - ); - await provider.events["action.started"]( - { - callId: "call-render", - idempotencyKey: "action:session-root:turn-0:call-render", - input: { - data: "data:image/png;base64,BwgJ", - mediaType: "image/png", - type: "file", - }, - name: "render", - scope: ATTEMPT_SCOPE, - type: "action.started", - }, - actionContext, - ); - await provider.events["action.completed"]( - { - idempotencyKey: "action:session-root:turn-0:call-render", - output: { - output: { - type: "content", - value: [ - { - data: new Uint8Array([10, 11, 12]), - mediaType: "image/png", - type: "image-data", - }, - ], + type: "result", }, - type: "result", + scope: ATTEMPT_SCOPE, + type: "action.completed", }, - scope: ATTEMPT_SCOPE, - type: "action.completed", - }, - actionContext, - ); + actionContext, + ); - const spans = (await backgroundLogger.drain()) as Array< - Record - >; - const step = spans.find( - (span) => span.span_attributes?.name === "eve.step", - ); - const inputAttachment = step?.input?.[0]?.content?.[0]?.data; - const imageAttachment = step?.input?.[0]?.content?.[1]?.image; - const unconvertedFile = step?.input?.[0]?.content?.[2]?.data; - const outputAttachment = - step?.input?.[1]?.content?.[0]?.output?.value?.[0]?.data; - const action = spans.find( - (span) => span.span_attributes?.name === "render", - ); - const actionInputAttachment = action?.input?.data; - const actionOutputAttachment = action?.output?.value?.[0]?.data; - expect(inputAttachment).toBeInstanceOf(Attachment); - expect(inputAttachment.reference).toMatchObject({ - content_type: "image/png", - filename: "tiny.png", - type: "braintrust_attachment", - }); - expect(imageAttachment).toBeInstanceOf(Attachment); - expect(imageAttachment.reference).toMatchObject({ - content_type: "image/png", - type: "braintrust_attachment", - }); - expect(unconvertedFile).toBe("not valid base64!"); - expect(outputAttachment).toBeInstanceOf(Attachment); - expect(outputAttachment.reference).toMatchObject({ - content_type: "image/png", - filename: "attachment.png", - type: "braintrust_attachment", - }); - expect(actionInputAttachment).toBeInstanceOf(Attachment); - expect(actionInputAttachment.reference).toMatchObject({ - content_type: "image/png", - type: "braintrust_attachment", - }); - expect(actionOutputAttachment).toBeInstanceOf(Attachment); - expect(actionOutputAttachment.reference).toMatchObject({ - content_type: "image/png", - type: "braintrust_attachment", - }); - expect(JSON.stringify(step?.input)).not.toContain("AQID"); - expect(JSON.stringify(step?.input)).not.toContain("BAUG"); - }); + const spans = (await backgroundLogger.drain()) as Array< + Record + >; + const step = spans.find( + (span) => span.span_attributes?.name === "eve.step", + ); + expect(step).toMatchObject({ + root_span_id: persistedTurn.rootSpanId, + span_parents: [persistedTurn.spanId], + }); + const inputAttachment = step?.input?.[0]?.content?.[0]?.data; + const imageAttachment = step?.input?.[0]?.content?.[1]?.image; + const unconvertedFile = step?.input?.[0]?.content?.[2]?.data; + const outputAttachment = + step?.input?.[1]?.content?.[0]?.output?.value?.[0]?.data; + const action = spans.find( + (span) => span.span_attributes?.name === "render", + ); + const actionInputAttachment = action?.input?.data; + const actionOutputAttachment = action?.output?.value?.[0]?.data; + if (captureAttachments) { + expect(inputAttachment).toBeInstanceOf(Attachment); + expect(inputAttachment.reference).toMatchObject({ + content_type: "image/png", + filename: "tiny.png", + type: "braintrust_attachment", + }); + expect(imageAttachment).toBeInstanceOf(Attachment); + expect(imageAttachment.reference).toMatchObject({ + content_type: "image/png", + type: "braintrust_attachment", + }); + expect(unconvertedFile).toBe("not valid base64!"); + expect(outputAttachment).toBeInstanceOf(Attachment); + expect(outputAttachment.reference).toMatchObject({ + content_type: "image/png", + filename: "attachment.png", + type: "braintrust_attachment", + }); + } else { + expect(inputAttachment).toBeUndefined(); + expect(imageAttachment).toBeUndefined(); + expect(unconvertedFile).toBeUndefined(); + expect(outputAttachment).toBeUndefined(); + for (const payload of ["AQID", "DQ4P", "BAUG", "not valid base64!"]) { + expect(decode).not.toHaveBeenCalledWith(payload); + } + } + expect(step?.input?.[0]?.content?.[3]).toEqual({ + type: "image", + image: "https://example.com/image.png", + }); + expect(action?.input).toEqual({ type: "file", data: "report text" }); + expect(actionInputAttachment).toBe("report text"); + expect(actionOutputAttachment).toBe("ordinary JSON data"); + expect(JSON.stringify(step?.input)).not.toContain("AQID"); + expect(JSON.stringify(step?.input)).not.toContain("BAUG"); + }, + ); it("preserves arbitrary action outputs with type fields", async () => { const provider = braintrustEveInstrumentation(); diff --git a/js/src/instrumentation/providers/eve-instrumentation.ts b/js/src/instrumentation/providers/eve-instrumentation.ts index d9c673a5e..1ac8af24c 100644 --- a/js/src/instrumentation/providers/eve-instrumentation.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.ts @@ -1,6 +1,9 @@ import { debugLogger } from "../../debug-logger"; import { Attachment, + BaseAttachment, + CAPTURE_ATTACHMENTS, + _internalCaptureAttachmentsEnabled, NOOP_SPAN, _internalStartSpanWithInitialMerge, currentLogger, @@ -19,7 +22,6 @@ import { getCurrentUnixTimestamp } from "../../util"; import { convertDataToBlob, getExtensionFromMediaType, - processInputAttachments, } from "../../wrappers/attachment-utils"; import type { EveInstrumentationActionCompletedEvent, @@ -43,6 +45,7 @@ import type { type EveSpan = Pick; type EveSpanReference = { + readonly captureAttachments: boolean; readonly exported: string; readonly rootSpanId: string; readonly spanId: string; @@ -56,6 +59,7 @@ type TurnState = { rootSpanId: string; rowId: string; span: EveSpan; + captureAttachments: boolean; spanId: string; }; @@ -132,6 +136,10 @@ class ResumedEveSpan implements EveSpan { constructor(private readonly reference: EveSpanReference) {} + get [CAPTURE_ATTACHMENTS](): boolean { + return this.reference.captureAttachments; + } + get rootSpanId(): string { return this.reference.rootSpanId; } @@ -222,7 +230,7 @@ class EveBridge { ...this.metadata(event.scope.sessionId), ...modelMetadata(event.model.modelId, event.model.provider), }; - const input = capturedModelInput(event.input); + const input = capturedModelInput(event.input, turn.captureAttachments); const ids = await generateEveIds("step", event.idempotencyKey); const span = await this.startEveSpan(context, { event: { @@ -231,6 +239,7 @@ class EveBridge { metadata, }, name: "eve.step", + [CAPTURE_ATTACHMENTS]: turn.captureAttachments, parentSpanIds: { rootSpanId: turn.rootSpanId, spanId: turn.spanId, @@ -320,7 +329,7 @@ class EveBridge { ): Promise { const turn = await this.turnForScope(event.scope, context); const metadata = turn.metadata; - const input = processInputAttachments(event.input); + const input = event.input; const ids = await generateEveIds( "action", event.scope.sessionId, @@ -334,6 +343,7 @@ class EveBridge { metadata, }, name: event.name, + [CAPTURE_ATTACHMENTS]: turn.captureAttachments, parentSpanIds: { rootSpanId: turn.rootSpanId, spanId: turn.spanId, @@ -353,7 +363,7 @@ class EveBridge { error: toError(event.output.error, "Eve action failed"), }); } else if (event.output.output !== undefined) { - span.log({ output: processInputAttachments(event.output.output) }); + span.log({ output: event.output.output }); } span.end(actionEndArgs(event.acceptedAtMs)); } @@ -402,6 +412,7 @@ class EveBridge { context: EveInstrumentationHandlerContext, metadata: Record, ): Promise { + const captureAttachments = _internalCaptureAttachmentsEnabled(); const ids = await generateEveIds("turn", event.sessionId, event.turnId); const parentSpanId = event.parentLineage === undefined @@ -441,6 +452,8 @@ class EveBridge { return await this.startEveSpan(context, { event: { id: ids.rowId, metadata }, name: "eve.turn", + [CAPTURE_ATTACHMENTS]: + parentTurn?.captureAttachments ?? captureAttachments, parentSpanIds: parentSpanId === undefined ? { parentSpanIds: [], rootSpanId } @@ -452,7 +465,9 @@ class EveBridge { private async startEveSpan( context: EveInstrumentationHandlerContext, - args: Parameters[0], + args: Parameters[0] & { + [CAPTURE_ATTACHMENTS]: boolean; + }, ): Promise { const reference = readSpanReference(context.state.get()); if (reference !== undefined) { @@ -471,6 +486,7 @@ class EveBridge { try { const reference: EveSpanReference = { + captureAttachments: _internalCaptureAttachmentsEnabled(span), exported: await span.export(), rootSpanId: span.rootSpanId, spanId: span.spanId, @@ -507,6 +523,7 @@ class EveBridge { rootSpanId: span.rootSpanId, rowId: ids.rowId, span, + captureAttachments: _internalCaptureAttachmentsEnabled(span), spanId: span.spanId, }; this.turnsByKey.set(key, turn); @@ -559,6 +576,7 @@ class EveBridge { rootSpanId: span.rootSpanId, rowId: ids.rowId, span, + captureAttachments: _internalCaptureAttachmentsEnabled(span), spanId: span.spanId, }; } @@ -578,9 +596,8 @@ class EveBridge { } const ids = await generateEveIds("turn", scope.sessionId, scope.turnId); - const persistedRootSpanId = readSpanReference( - context.state.get(), - )?.rootSpanId; + const reference = readSpanReference(context.state.get()); + const persistedRootSpanId = reference?.rootSpanId; const rememberedRootSpanId = eveRootSpanIdsByTurnKey.get(key); return { key, @@ -594,6 +611,8 @@ class EveBridge { scope.turnId, )), rowId: ids.rowId, + captureAttachments: + reference?.captureAttachments ?? _internalCaptureAttachmentsEnabled(), span: NOOP_SPAN, spanId: ids.spanId, }; @@ -677,6 +696,7 @@ class EveBridge { metadata: this.metadata(event.scope.sessionId), }, name: "eve.step", + [CAPTURE_ATTACHMENTS]: turn.captureAttachments, parentSpanIds: { rootSpanId: turn.rootSpanId, spanId: turn.spanId, @@ -707,6 +727,7 @@ class EveBridge { return await this.startEveSpan(context, { event: { id: ids.rowId, metadata: turn.metadata }, name: "eve.action", + [CAPTURE_ATTACHMENTS]: turn.captureAttachments, parentSpanIds: { rootSpanId: turn.rootSpanId, spanId: turn.spanId, @@ -731,7 +752,10 @@ function readSpanReference(value: unknown): EveSpanReference | undefined { const exported = value["exported"]; const rootSpanId = value["rootSpanId"]; const spanId = value["spanId"]; + const captureAttachments = value["captureAttachments"]; if ( + (captureAttachments !== undefined && + typeof captureAttachments !== "boolean") || typeof exported !== "string" || typeof rootSpanId !== "string" || typeof spanId !== "string" @@ -739,6 +763,9 @@ function readSpanReference(value: unknown): EveSpanReference | undefined { return undefined; } return { + // State written before attachment capture was configurable has no policy. + captureAttachments: + captureAttachments ?? _internalCaptureAttachmentsEnabled(), exported, rootSpanId, spanId, @@ -754,6 +781,7 @@ function readSpanReference(value: unknown): EveSpanReference | undefined { function capturedModelInput( input: EveInstrumentationModelInput | undefined, + captureAttachments: boolean, ): unknown[] | undefined { if (input === undefined) { return undefined; @@ -763,19 +791,22 @@ function capturedModelInput( value.push({ content: input.instructions, role: "system" }); } else if (Array.isArray(input.instructions)) { for (const instruction of input.instructions) { - const projected = projectedModelMessage(instruction); + const projected = projectedModelMessage(instruction, captureAttachments); if (projected?.role === "system") { value.push(projected); } } } else if (isObject(input.instructions)) { - const instruction = projectedModelMessage(input.instructions); + const instruction = projectedModelMessage( + input.instructions, + captureAttachments, + ); if (instruction?.role === "system") { value.push(instruction); } } for (const message of input.messages) { - const projected = projectedModelMessage(message); + const projected = projectedModelMessage(message, captureAttachments); if (projected !== undefined) { value.push(projected); } @@ -785,6 +816,7 @@ function capturedModelInput( function projectedModelMessage( value: unknown, + captureAttachments: boolean, ): { content: unknown; role: string } | undefined { if (!isObject(value)) { return undefined; @@ -826,7 +858,12 @@ function projectedModelMessage( ? part["mediaType"] : "image/png"; projected.push({ - image: mediaAttachment(part["image"], mediaType, undefined), + image: mediaAttachment( + part["image"], + mediaType, + undefined, + captureAttachments, + ), ...(typeof part["mediaType"] === "string" ? { mediaType } : {}), type: "image", }); @@ -846,6 +883,7 @@ function projectedModelMessage( typeof part["filename"] === "string" ? part["filename"] : undefined, + captureAttachments, ); projected.push({ ...(data === undefined ? {} : { data }), @@ -876,7 +914,7 @@ function projectedModelMessage( typeof part["toolName"] === "string" ) { projected.push({ - output: projectedToolOutput(part["output"]), + output: projectedToolOutput(part["output"], captureAttachments), toolCallId: part["toolCallId"], toolName: part["toolName"], type: "tool-result", @@ -905,7 +943,10 @@ function projectedModelMessage( return { content: projected, role }; } -function projectedToolOutput(value: unknown): unknown { +function projectedToolOutput( + value: unknown, + captureAttachments: boolean, +): unknown { if (!isObject(value)) { return value; } @@ -945,6 +986,7 @@ function projectedToolOutput(value: unknown): unknown { part["data"], part["mediaType"], typeof part["filename"] === "string" ? part["filename"] : undefined, + captureAttachments, ); content.push({ ...(data === undefined ? {} : { data }), @@ -975,7 +1017,15 @@ function mediaAttachment( data: unknown, contentType: string, filename: string | undefined, + captureAttachments: boolean, ): unknown { + if ( + data instanceof BaseAttachment || + (data instanceof URL && data.protocol !== "data:") || + (typeof data === "string" && /^https?:/i.test(data)) + ) + return data; + if (!captureAttachments) return undefined; const blob = convertDataToBlob(data, contentType); if (blob === null) { return data; diff --git a/js/src/instrumentation/providers/google-genai-instrumentation.test.ts b/js/src/instrumentation/providers/google-genai-instrumentation.test.ts index 2d5fb2998..b495cbf65 100644 --- a/js/src/instrumentation/providers/google-genai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.test.ts @@ -19,6 +19,9 @@ vi.mock("../../isomorph", () => ({ }; }), newTracingChannel: vi.fn(), + getEnv: vi.fn((name: string) => + name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, + ), }, })); @@ -30,24 +33,31 @@ const mockNewTracingChannel = iso.newTracingChannel as ReturnType; const mockStartSpan = vi.mocked(startSpan); // Mock logger -vi.mock("../../logger", () => ({ - startSpan: vi.fn(() => ({ - log: vi.fn(), - end: vi.fn(), - })), - _internalGetGlobalState: vi.fn(() => undefined), - currentSpan: vi.fn(() => undefined), - withCurrent: vi.fn((_span: unknown, callback: () => unknown) => callback()), - Attachment: class MockAttachment { - reference: any; - constructor(params: any) { - this.reference = { - filename: params.filename, - content_type: params.contentType, - }; - } - }, -})); +vi.mock("../../logger", async () => { + const { default: iso } = await import("../../isomorph"); + return { + _internalCaptureAttachmentsEnabled: () => + iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", + BaseAttachment: class {}, + CAPTURE_ATTACHMENTS: Symbol.for("braintrust.captureAttachments"), + startSpan: vi.fn(() => ({ + log: vi.fn(), + end: vi.fn(), + })), + _internalGetGlobalState: vi.fn(() => undefined), + currentSpan: vi.fn(() => undefined), + withCurrent: vi.fn((_span: unknown, callback: () => unknown) => callback()), + Attachment: class MockAttachment { + reference: any; + constructor(params: any) { + this.reference = { + filename: params.filename, + content_type: params.contentType, + }; + } + }, + }; +}); describe("registerGoogleGenAIInstrumentation", () => { let mockChannel: any; @@ -1088,6 +1098,132 @@ describe("registerGoogleGenAIInstrumentation", () => { }); describe("interactions.create channel subscription", () => { + it.each([ + [false, false], + [false, true], + [true, false], + [true, true], + ])( + "preserves application JSON while processing media with capture=%s, streaming=%s", + async (captureAttachments, streaming) => { + const getEnv = vi.mocked(iso.getEnv); + const originalGetEnv = getEnv.getMockImplementation()!; + getEnv.mockImplementation((name) => + name === "BRAINTRUST_CAPTURE_ATTACHMENTS" + ? String(captureAttachments) + : undefined, + ); + try { + registerGoogleGenAIInstrumentation(); + const handlers = subscribeSpy.mock.calls[3][0]; + const data = { mimeType: "application/json", data: { count: 1 } }; + const mediaShapedJSON = { + type: "image", + mime_type: "image/png", + data: "ordinary application data", + }; + const media = { type: "image", mime_type: "image/png", data: "AQID" }; + const content = [ + { + type: "function_call", + name: "report", + arguments: { data, mediaShapedJSON }, + }, + { + type: "function_result", + name: "report", + result: { data, mediaShapedJSON }, + }, + media, + ]; + const result = { + id: "interaction-json", + input: content, + steps: [ + ...content.slice(0, 2), + { type: "model_output", content: [media] }, + ], + metadata: { data, mediaShapedJSON }, + }; + const event: any = { + arguments: [ + { + model: "gemini-2.5-flash", + input: content, + agent_config: { data, mediaShapedJSON }, + }, + ], + result: streaming + ? (async function* () { + for (const [index, step] of result.steps.entries()) { + yield { event_type: "step.start", index, step }; + } + yield { + event_type: "interaction.completed", + interaction: result, + }; + })() + : result, + }; + handlers.start(event); + const span = mockStartSpan.mock.results.at(-1)!.value; + handlers.asyncEnd(event); + if (streaming) { + for await (const _chunk of event.result) { + // Consume the provider stream to trigger final trace aggregation. + } + } + const expectedContent = [ + ...content.slice(0, 2), + { + type: "image", + mime_type: "image/png", + ...(captureAttachments + ? { + data: expect.objectContaining({ + reference: { + filename: "file.png", + content_type: "image/png", + }, + }), + } + : {}), + }, + ]; + expect(span.log).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + input: expect.objectContaining({ + input: expectedContent, + agent_config: { data, mediaShapedJSON }, + }), + metadata: expect.objectContaining({ + agent_config: { data, mediaShapedJSON }, + }), + }), + ); + const expectedSteps = [ + ...content.slice(0, 2), + { type: "model_output", content: [expectedContent[2]] }, + ]; + expect(span.log).toHaveBeenLastCalledWith( + expect.objectContaining({ + output: { + ...result, + input: expectedContent, + steps: streaming + ? expectedSteps.map((step, index) => ({ ...step, index })) + : expectedSteps, + }, + }), + ); + expect(media.data).toBe("AQID"); + } finally { + getEnv.mockImplementation(originalGetEnv); + } + }, + ); + it("subscribes to the interactions.create channel", () => { registerGoogleGenAIInstrumentation(); diff --git a/js/src/instrumentation/providers/google-genai-instrumentation.ts b/js/src/instrumentation/providers/google-genai-instrumentation.ts index 2a09d8df5..a0608b143 100644 --- a/js/src/instrumentation/providers/google-genai-instrumentation.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.ts @@ -1,6 +1,7 @@ import { uint8ArrayToBase64 } from "../../../util/bytes"; import { getExtensionFromMediaType, + omitMediaData, processInputAttachments, } from "../../wrappers/attachment-utils"; import { debugLogger } from "../../debug-logger"; @@ -12,8 +13,10 @@ import type { } from "../core/channel-definitions"; import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; import { + _internalCaptureAttachmentsEnabled, _internalGetGlobalState, Attachment, + CAPTURE_ATTACHMENTS, currentSpan, startSpan as startBaseSpan, withCurrent, @@ -67,6 +70,7 @@ type GenerateContentStreamEvent = googleGenAIInput?: Record; googleGenAIMetadata?: Record; googleGenAIStartTime?: number; + captureAttachments?: boolean; }; type SpanState = { @@ -195,7 +199,9 @@ class GoogleGenAIInstrumentationConsumer { spanState.startTime, ), ), - output: serializeGenerateContentOutput(event.result), + output: withCurrent(spanState.span, () => + serializeGenerateContentOutput(event.result), + ), }); } finally { spanState.span.end(); @@ -226,10 +232,12 @@ class GoogleGenAIInstrumentationConsumer { streamEvent.googleGenAIMetadata = extractGenerateContentMetadata(params); streamEvent.googleGenAIStartTime = getCurrentUnixTimestamp(); + streamEvent.captureAttachments = _internalCaptureAttachmentsEnabled(); }, asyncEnd: (event) => { const streamEvent = event as GenerateContentStreamEvent; patchGoogleGenAIStreamingResult({ + captureAttachments: streamEvent.captureAttachments, input: streamEvent.googleGenAIInput, metadata: streamEvent.googleGenAIMetadata, startTime: streamEvent.googleGenAIStartTime, @@ -502,7 +510,9 @@ function interceptGoogleGenAIMediaCall< try { void Promise.resolve(result).then((response) => { try { - span.log({ output: serializeOutput(response, params) }); + span.log({ + output: withCurrent(span, () => serializeOutput(response, params)), + }); } catch (error) { debugLogger.error( `Error capturing Google GenAI ${name} output:`, @@ -588,12 +598,19 @@ function logErrorAndEndSpan( } function patchGoogleGenAIStreamingResult(args: { + captureAttachments?: boolean; input: Record | undefined; metadata: Record | undefined; startTime: number | undefined; result: unknown; }): boolean { - const { input, metadata, result, startTime } = args; + const { + input, + metadata, + result, + startTime, + captureAttachments = _internalCaptureAttachmentsEnabled(), + } = args; if ( !input || @@ -618,6 +635,7 @@ function patchGoogleGenAIStreamingResult(args: { withSpanInstrumentationName( { name: "generate_content_stream", + [CAPTURE_ATTACHMENTS]: captureAttachments, spanAttributes: { type: SpanTypeAttribute.LLM, }, @@ -739,7 +757,35 @@ function patchGoogleGenAIStreamingResult(args: { if (firstTokenTime === null) { firstTokenTime = getCurrentUnixTimestamp(); } - chunks.push(nextResult.value); + chunks.push( + captureAttachments + ? nextResult.value + : { + ...nextResult.value, + candidates: nextResult.value.candidates?.map( + (candidate) => ({ + ...candidate, + content: candidate.content + ? { + ...candidate.content, + parts: candidate.content.parts?.map((part) => + part.inlineData + ? { + ...part, + // This copy is only retained for trace aggregation. + inlineData: omitMediaData( + part.inlineData, + "data", + ) as GoogleGenAIPart["inlineData"], + } + : part, + ), + } + : candidate.content, + }), + ), + }, + ); } if (nextResult.done) { @@ -748,6 +794,7 @@ function patchGoogleGenAIStreamingResult(args: { chunks, requestStartTime, firstTokenTime, + captureAttachments, ), }); } @@ -775,6 +822,7 @@ function patchGoogleGenAIStreamingResult(args: { chunks, requestStartTime, firstTokenTime, + captureAttachments, ), }); } else { @@ -850,7 +898,9 @@ function serializeGenerateContentOutput( ? { content: { ...candidate.content, - parts: candidate.content.parts.map((part) => serializePart(part)), + parts: candidate.content.parts + .map((part) => serializePart(part)) + .filter((part) => part !== undefined), }, } : {}), @@ -1231,7 +1281,7 @@ function serializeInteractionInput( ]) { const value = params[key]; if (value !== undefined) { - input[key] = serializeInteractionValue(value); + input[key] = serializeInteractionValue(value, false); } } @@ -1256,13 +1306,13 @@ function extractInteractionMetadata( ]) { const value = params[key]; if (value !== undefined) { - metadata[key] = serializeInteractionValue(value); + metadata[key] = serializeInteractionValue(value, false); } } if (Array.isArray(params.tools)) { metadata.tools = params.tools.map((tool) => - serializeInteractionValue(tool), + serializeInteractionValue(tool, false), ); } @@ -1294,7 +1344,9 @@ function serializeContentItem(item: string | GoogleGenAIContent): unknown { if (item.parts && Array.isArray(item.parts)) { return { ...item, - parts: item.parts.map((part: GoogleGenAIPart) => serializePart(part)), + parts: item.parts + .map((part: GoogleGenAIPart) => serializePart(part)) + .filter((part) => part !== undefined), }; } return item; @@ -1310,14 +1362,27 @@ function serializeContentItem(item: string | GoogleGenAIContent): unknown { /** * Serialize a part, converting inline data to Attachments. */ -function serializePart(part: GoogleGenAIPart): unknown { +function serializePart( + part: GoogleGenAIPart, + captureAttachments = _internalCaptureAttachmentsEnabled(), +): unknown { if (!part || typeof part !== "object") { return part; } + if (part.inlineData && !captureAttachments) + return omitMediaData({ + ...part, + inlineData: omitMediaData(part.inlineData, "data"), + }); if (part.inlineData && part.inlineData.data) { const { data, mimeType } = part.inlineData; - const attachment = createAttachmentFromInlineData(data, mimeType); + const attachment = createAttachmentFromInlineData( + data, + mimeType, + undefined, + captureAttachments, + ); if (attachment) { return mimeType.startsWith("image/") @@ -1510,6 +1575,7 @@ function serializeVideoInteractionOutput( function serializeInteractionValue( value: unknown, + processMedia = true, seen = new WeakSet(), ): unknown { if (value === null || value === undefined || typeof value !== "object") { @@ -1517,7 +1583,9 @@ function serializeInteractionValue( } if (Array.isArray(value)) { - return value.map((item) => serializeInteractionValue(item, seen)); + return value.map((item) => + serializeInteractionValue(item, processMedia, seen), + ); } const dict: unknown = tryToDict(value); @@ -1526,7 +1594,9 @@ function serializeInteractionValue( } if (Array.isArray(dict)) { - return dict.map((item) => serializeInteractionValue(item, seen)); + return dict.map((item) => + serializeInteractionValue(item, processMedia, seen), + ); } if (seen.has(dict)) { @@ -1536,22 +1606,48 @@ function serializeInteractionValue( seen.add(dict); try { const serialized: Record = {}; + const isMedia = + processMedia && + "type" in dict && + typeof dict.type === "string" && + ["image", "audio", "video", "document"].includes(dict.type); const mimeType = "mime_type" in dict && typeof dict.mime_type === "string" ? dict.mime_type : "mimeType" in dict && typeof dict.mimeType === "string" ? dict.mimeType : undefined; + const captureAttachments = isMedia && _internalCaptureAttachmentsEnabled(); const attachment = - mimeType && "data" in dict && dict.data !== undefined + captureAttachments && + mimeType && + "data" in dict && + dict.data !== undefined ? createAttachmentFromInlineData(dict.data, mimeType) : null; - for (const [key, entry] of Object.entries(dict)) { + for (const key of Object.keys(dict)) { + if (key === "data" && isMedia && !captureAttachments) continue; + const entry: unknown = Reflect.get(dict, key); if (key === "data" && attachment) { serialized[key] = attachment; } else { - serialized[key] = serializeInteractionValue(entry, seen); + // Only descend into provider content containers for media conversion. + // Tool arguments/results and metadata remain ordinary application JSON. + serialized[key] = serializeInteractionValue( + entry, + processMedia && + [ + "input", + "content", + "steps", + "outputs", + "output_image", + "output_audio", + "output_video", + ].includes(key), + seen, + ); } } @@ -1565,7 +1661,9 @@ function createAttachmentFromInlineData( data: unknown, mimeType?: string, filename = `file.${mimeType ? getExtensionFromMediaType(mimeType) : "bin"}`, + captureAttachments = _internalCaptureAttachmentsEnabled(), ): Attachment | null { + if (!captureAttachments) return null; if ( !( data instanceof Uint8Array || @@ -1916,6 +2014,7 @@ function aggregateGenerateContentChunks( chunks: GoogleGenAIGenerateContentResponse[], startTime: number, firstTokenTime: number | null, + captureAttachments: boolean, ): { aggregated: Record; metrics: Record; @@ -1971,7 +2070,9 @@ function aggregateGenerateContentChunks( } else if (part.executableCode) { otherParts.push({ executableCode: part.executableCode }); } else if (part.inlineData || part.fileData) { - const serializedPart = tryToDict(serializePart(part)); + const serializedPart = tryToDict( + serializePart(part, captureAttachments), + ); if (serializedPart) { otherParts.push(serializedPart); } @@ -2120,7 +2221,7 @@ function aggregateInteractionEvents( output.output_text = outputText; } if (latestUsage) { - output.usage = serializeInteractionValue(latestUsage); + output.usage = serializeInteractionValue(latestUsage, false); } const compactSteps = Array.from(steps.values()).sort( @@ -2182,7 +2283,10 @@ function compactInteractionStep(step: unknown): Record { "is_error", ]) { if (stepDict[key] !== undefined) { - compact[key] = serializeInteractionValue(stepDict[key]); + compact[key] = serializeInteractionValue( + stepDict[key], + key === "content", + ); } } diff --git a/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts b/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts index b73680cc3..4b669aca1 100644 --- a/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts @@ -22,6 +22,7 @@ describe("Google Generative AI instrumentation", () => { beforeEach(() => { logger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ + captureAttachments: true, projectName: "tmp-luca-google-generative-ai-tests", projectId: "test-project-id", }); @@ -366,23 +367,35 @@ describe("Google Generative AI instrumentation", () => { ]); }); - it("converts image inputs without mutating the provider request", async () => { - const inlineData = { data: "aGVsbG8=", mimeType: "image/png" }; - const request = [{ inlineData }]; - await channels.generateContent.invoke( - async () => ({ response: {} }), - { model: "models/test" }, - [request], - {}, - ); - const spans = await logger.drain(); - expect(spans[0]).toHaveProperty( - "input.contents.0.parts.0.inlineData.data", - expect.any(Attachment), - ); - expect(request[0].inlineData).toBe(inlineData); - expect(inlineData.data).toBe("aGVsbG8="); - }); + it.each([false, true])( + "applies capture=%s to image inputs without mutating the provider request", + async (captureAttachments) => { + initLogger({ + projectName: "tmp-luca-google-generative-ai-tests", + projectId: "test-project-id", + captureAttachments, + }); + const inlineData = { data: "aGVsbG8=", mimeType: "image/png" }; + const request = [{ inlineData }]; + await channels.generateContent.invoke( + async () => ({ response: {} }), + { model: "models/test" }, + [request], + {}, + ); + const spans = await logger.drain(); + if (captureAttachments) { + expect(spans[0]).toHaveProperty( + "input.contents.0.parts.0.inlineData.data", + expect.any(Attachment), + ); + } else { + expect(spans[0]).toHaveProperty("input.contents.0.parts", []); + } + expect(request[0].inlineData).toBe(inlineData); + expect(inlineData.data).toBe("aGVsbG8="); + }, + ); it("keeps all original input when any attachment cannot be converted", async () => { const request = [ diff --git a/js/src/instrumentation/providers/google-generative-ai-instrumentation.ts b/js/src/instrumentation/providers/google-generative-ai-instrumentation.ts index 85e45685a..de1c6852b 100644 --- a/js/src/instrumentation/providers/google-generative-ai-instrumentation.ts +++ b/js/src/instrumentation/providers/google-generative-ai-instrumentation.ts @@ -1,3 +1,4 @@ +import { _internalCaptureAttachmentsEnabled } from "../../logger"; import { SpanTypeAttribute } from "../../../util/index"; import { debugLogger } from "../../debug-logger"; import { startSpan, withCurrent, type Span } from "../../logger"; @@ -6,7 +7,10 @@ import { withSpanInstrumentationName, } from "../../span-origin"; import { getCurrentUnixTimestamp, isObject } from "../../util"; -import { processInputAttachments } from "../../wrappers/attachment-utils"; +import { + omitMediaData, + processInputAttachments, +} from "../../wrappers/attachment-utils"; import type { GenerativeAIChat, GenerativeAIConfig, @@ -381,10 +385,13 @@ function extractInput( // Convert Google's inlineData to the shared attachment processor's file shape, // then restore the native payload shape. Any conversion failure keeps all input. function convertAttachments(value: T): T { + const captureAttachments = _internalCaptureAttachmentsEnabled(); const convert = (node: unknown): unknown => { - if (Array.isArray(node)) return node.map(convert); + if (Array.isArray(node)) + return node.map(convert).filter((item) => item !== undefined); if (!isObject(node)) return node; if (isObject(node.inlineData)) { + if (!captureAttachments) return omitMediaData(node, "inlineData"); const { data, mimeType } = node.inlineData; const processed = processInputAttachments({ type: "file", diff --git a/js/src/instrumentation/providers/groq-instrumentation.ts b/js/src/instrumentation/providers/groq-instrumentation.ts index c04f8d24d..610076cfe 100644 --- a/js/src/instrumentation/providers/groq-instrumentation.ts +++ b/js/src/instrumentation/providers/groq-instrumentation.ts @@ -7,7 +7,12 @@ import { isObject, SpanTypeAttribute, } from "../../../util/index"; -import { Attachment, withCurrent, type Span } from "../../logger"; +import { + _internalCaptureAttachmentsEnabled, + Attachment, + withCurrent, + type Span, +} from "../../logger"; import { convertDataToBlob, getExtensionFromMediaType, @@ -126,7 +131,10 @@ function extractGroqAudioInput( span: Span, ): { input: unknown; metadata: Record } { const source = params.file ?? params.url; - const filePart = groqAudioFilePart(source); + const filePart = groqAudioFilePart( + source, + _internalCaptureAttachmentsEnabled(span), + ); const input = { operation, ...(params.prompt !== undefined ? { prompt: params.prompt } : {}), @@ -149,11 +157,15 @@ function extractGroqAudioInput( }; } -function groqAudioFilePart(source: unknown): unknown | undefined { +function groqAudioFilePart( + source: unknown, + captureAttachments: boolean, +): unknown | undefined { if (source === undefined || source === null) return undefined; if (typeof source === "string") { if (source.startsWith("data:")) { + if (!captureAttachments) return undefined; const contentType = source.match(/^data:([^;,]+)/)?.[1]; const blob = contentType ? convertDataToBlob(source, contentType) : null; if (contentType && blob) { @@ -175,7 +187,7 @@ function groqAudioFilePart(source: unknown): unknown | undefined { }; } - if (!isObject(source)) return undefined; + if (!captureAttachments || !isObject(source)) return undefined; const filename = typeof source.name === "string" && source.name ? filenameFromPath(source.name) @@ -209,7 +221,7 @@ function observeGroqAudioInput( input: Record, span: Span, ): void { - if (!isObject(source)) return; + if (!_internalCaptureAttachmentsEnabled(span) || !isObject(source)) return; const body = isObject(source.body) ? source.body @@ -361,6 +373,7 @@ function captureGroqSpeechResponse( startTime: number, ): boolean { if (!isObject(response)) return false; + const captureAttachments = _internalCaptureAttachmentsEnabled(span); const headerContentType = responseHeader(response, "content-type")?.split( ";", @@ -399,7 +412,7 @@ function captureGroqSpeechResponse( }); firstChunk = false; } - chunks.push(new Uint8Array(chunk)); + if (captureAttachments) chunks.push(new Uint8Array(chunk)); }; const complete = () => { if (finished) return; diff --git a/js/src/instrumentation/providers/ollama-instrumentation.test.ts b/js/src/instrumentation/providers/ollama-instrumentation.test.ts index 4503d4ddf..74bad0b46 100644 --- a/js/src/instrumentation/providers/ollama-instrumentation.test.ts +++ b/js/src/instrumentation/providers/ollama-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import iso from "../../isomorph"; import { configureNode } from "../../node/config"; import { @@ -12,6 +12,8 @@ import { } from "./ollama-instrumentation"; configureNode(); +beforeEach(() => vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", "true")); +afterEach(() => vi.unstubAllEnvs()); describe("Ollama instrumentation extraction", () => { it("normalizes chat inputs, tools, and supported request metadata", () => { diff --git a/js/src/instrumentation/providers/ollama-instrumentation.ts b/js/src/instrumentation/providers/ollama-instrumentation.ts index 405b3dff9..17d5c23c5 100644 --- a/js/src/instrumentation/providers/ollama-instrumentation.ts +++ b/js/src/instrumentation/providers/ollama-instrumentation.ts @@ -1,6 +1,6 @@ import { SpanTypeAttribute, isObject } from "../../../util/index"; import iso from "../../isomorph"; -import { Attachment } from "../../logger"; +import { _internalCaptureAttachmentsEnabled, Attachment } from "../../logger"; import { processInputAttachments } from "../../wrappers/attachment-utils"; import type { OllamaChatRequest, @@ -199,7 +199,17 @@ function normalizeTextAndImages( const imageParts: Record[] = []; const unrecognizedImages: unknown[] = []; + const captureAttachments = _internalCaptureAttachmentsEnabled(); for (const image of images) { + if (!captureAttachments) { + if ( + image instanceof URL || + (typeof image === "string" && /^https?:\/\//i.test(image)) + ) { + imageParts.push({ type: "image_url", image_url: { url: image } }); + } + continue; + } let localImagePath: string | undefined; let localPathMediaType: string | undefined; if ( diff --git a/js/src/instrumentation/providers/openai-agents-trace-processor.ts b/js/src/instrumentation/providers/openai-agents-trace-processor.ts index a335f2efb..42d5c32b4 100644 --- a/js/src/instrumentation/providers/openai-agents-trace-processor.ts +++ b/js/src/instrumentation/providers/openai-agents-trace-processor.ts @@ -6,7 +6,10 @@ import { NOOP_SPAN, currentSpan, startSpan as startBaseSpan, + withCurrent, } from "../../logger"; +import { processInputAttachments } from "../../wrappers/attachment-utils"; +import { processImagesInOutput } from "./openai-span-data"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -325,7 +328,9 @@ export class OpenAIAgentsTraceProcessor { return Promise.resolve(); } - const logData = this.extractLogData(span); + const logData = withCurrent(braintrustSpan, () => + this.extractLogData(span), + ); braintrustSpan.log({ error: span.error, ...logData, @@ -415,8 +420,8 @@ export class OpenAIAgentsTraceProcessor { }; return { - input: spanData._input, - output, + input: processInputAttachments(spanData._input), + output: processInputAttachments(processImagesInOutput(output)), metadata: isObject(response) ? this.omitKeys(response, ["output", "usage"]) : {}, @@ -459,8 +464,8 @@ export class OpenAIAgentsTraceProcessor { span: OpenAIAgentsSpan, ): Record { return { - input: spanData.input, - output: spanData.output, + input: processInputAttachments(spanData.input), + output: processInputAttachments(processImagesInOutput(spanData.output)), metadata: { model: spanData.model, model_config: spanData.model_config, @@ -493,7 +498,10 @@ export class OpenAIAgentsTraceProcessor { spanData: OpenAIAgentsTranscriptionSpanData, ): Record { return { - input: spanData.input, + input: processInputAttachments({ + type: "input_audio", + input_audio: spanData.input, + })?.input_audio, output: spanData.output, metadata: { model: spanData.model, @@ -507,7 +515,10 @@ export class OpenAIAgentsTraceProcessor { ): Record { return { input: spanData.input, - output: spanData.output, + output: processInputAttachments({ + type: "input_audio", + input_audio: spanData.output, + })?.input_audio, metadata: { model: spanData.model, model_config: spanData.model_config, diff --git a/js/src/instrumentation/providers/openai-instrumentation.test.ts b/js/src/instrumentation/providers/openai-instrumentation.test.ts index 90e162c6b..6827b88fe 100644 --- a/js/src/instrumentation/providers/openai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openai-instrumentation.test.ts @@ -1316,9 +1316,21 @@ describe("processImagesInOutput", () => { }; expect(processImagesInOutput(output, false)).toEqual({ - ...output, - result: "", + type: "image_generation_call", + output_format: "png", + revised_prompt: "A red pixel", }); + expect( + processImagesInOutput( + [ + { type: "image_generation_call", result: "AQID" }, + { type: "message", content: [{ type: "output_text", text: "done" }] }, + ], + false, + ), + ).toEqual([ + { type: "message", content: [{ type: "output_text", text: "done" }] }, + ]); }); describe("image_generation_call conversion", () => { @@ -1334,7 +1346,7 @@ describe("processImagesInOutput", () => { revised_prompt: "A red pixel", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result.type).toBe("image_generation_call"); expect(result.output_format).toBe("png"); @@ -1356,7 +1368,7 @@ describe("processImagesInOutput", () => { output_format: "jpg", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); const attachment = result.result as Attachment; expect(attachment.reference.filename).toBe("generated_image.jpg"); @@ -1373,7 +1385,7 @@ describe("processImagesInOutput", () => { output_format: "webp", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); const attachment = result.result as Attachment; expect(attachment.reference.filename).toBe("generated_image.webp"); @@ -1389,7 +1401,7 @@ describe("processImagesInOutput", () => { result: base64Image, }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); const attachment = result.result as Attachment; expect(attachment.reference.filename).toBe("generated_image.png"); @@ -1406,7 +1418,7 @@ describe("processImagesInOutput", () => { revised_prompt: "A beautiful sunset", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); const attachment = result.result as Attachment; expect(attachment.reference.filename).toContain("A_beautiful_sunset"); @@ -1425,7 +1437,7 @@ describe("processImagesInOutput", () => { revised_prompt: longPrompt, }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); const attachment = result.result as Attachment; // Should be truncated to 50 chars + sanitized + extension @@ -1442,7 +1454,7 @@ describe("processImagesInOutput", () => { revised_prompt: "Hello@World#Test!", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); const attachment = result.result as Attachment; expect(attachment.reference.filename).toBe("Hello_World_Test_.png"); @@ -1458,7 +1470,7 @@ describe("processImagesInOutput", () => { output_format: "jpg", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); const attachment = result.result as Attachment; expect(attachment.reference.filename).toBe("generated_image.jpg"); @@ -1477,7 +1489,7 @@ describe("processImagesInOutput", () => { another_field: 123, }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result.type).toBe("image_generation_call"); expect(result.output_format).toBe("png"); @@ -1501,7 +1513,7 @@ describe("processImagesInOutput", () => { { type: "text", content: "World" }, ]; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(Array.isArray(result)).toBe(true); expect(result[0]).toEqual({ type: "text", content: "Hello" }); @@ -1522,7 +1534,7 @@ describe("processImagesInOutput", () => { ], ]; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(Array.isArray(result)).toBe(true); expect(Array.isArray(result[0])).toBe(true); @@ -1546,7 +1558,7 @@ describe("processImagesInOutput", () => { }, ]; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result[0].result).toBeInstanceOf(Attachment); expect(result[1].result).toBeInstanceOf(Attachment); @@ -1568,7 +1580,7 @@ describe("processImagesInOutput", () => { content: "Hello world", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); }); @@ -1578,7 +1590,7 @@ describe("processImagesInOutput", () => { data: "some data", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); }); @@ -1588,7 +1600,7 @@ describe("processImagesInOutput", () => { content: "Generated text", }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); }); @@ -1621,7 +1633,7 @@ describe("processImagesInOutput", () => { result: null, }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); }); @@ -1631,7 +1643,7 @@ describe("processImagesInOutput", () => { result: undefined, }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); }); }); @@ -1643,7 +1655,7 @@ describe("processImagesInOutput", () => { result: 12345, // Not a string }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); expect(result.result).toBe(12345); }); @@ -1654,7 +1666,7 @@ describe("processImagesInOutput", () => { result: { data: "some data" }, }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); }); @@ -1664,7 +1676,7 @@ describe("processImagesInOutput", () => { result: ["data"], }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); }); @@ -1674,7 +1686,7 @@ describe("processImagesInOutput", () => { result: true, }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); expect(result).toEqual(output); }); @@ -1688,7 +1700,7 @@ describe("processImagesInOutput", () => { revised_prompt: 12345, // Not a string }; - const result = processImagesInOutput(output); + const result = processImagesInOutput(output, true); const attachment = result.result as Attachment; expect(attachment.reference.filename).toBe("generated_image.png"); diff --git a/js/src/instrumentation/providers/openai-instrumentation.ts b/js/src/instrumentation/providers/openai-instrumentation.ts index 521c5e553..49b3e54bf 100644 --- a/js/src/instrumentation/providers/openai-instrumentation.ts +++ b/js/src/instrumentation/providers/openai-instrumentation.ts @@ -1,3 +1,5 @@ +import { _internalCaptureAttachmentsEnabled } from "../../logger"; +import { processInputAttachments } from "../../wrappers/attachment-utils"; import { interceptOpenAIMedia } from "./openai-media"; import { traceAsyncChannel, @@ -71,9 +73,11 @@ export function registerOpenAIInstrumentation(): void { name: "Chat Completion", type: SpanTypeAttribute.LLM, extractInput: ([params]) => extractOpenAIChatInput(params), - extractOutput: (result) => { - return result?.choices; - }, + extractOutput: (result, event) => + processChatCompletionOutput( + result?.choices, + event?.arguments?.[0].audio?.format, + ), extractMetrics: (result, startTime, endEvent) => { const metrics = withCachedMetric( parseMetricsFromUsage(result?.usage), @@ -119,9 +123,11 @@ export function registerOpenAIInstrumentation(): void { name: "Chat Completion", type: SpanTypeAttribute.LLM, extractInput: ([params]) => extractOpenAIChatInput(params), - extractOutput: (result) => { - return result?.choices; - }, + extractOutput: (result, event) => + processChatCompletionOutput( + result?.choices, + event?.arguments?.[0].audio?.format, + ), extractMetrics: (result, startTime, endEvent) => { const metrics = withCachedMetric( parseMetricsFromUsage(result?.usage), @@ -246,6 +252,37 @@ export function registerOpenAIInstrumentation(): void { }); } +function processChatCompletionOutput( + choices: OpenAIChatChoice[] | undefined, + audioFormat: string | undefined, +): unknown { + const captureAttachments = _internalCaptureAttachmentsEnabled(); + return choices?.map((choice) => ({ + ...choice, + // Parsed outputs and tool arguments are application JSON, not media parts. + message: { + ...choice.message, + ...(Array.isArray(choice.message.content) + ? { + content: processInputAttachments( + choice.message.content, + captureAttachments, + ), + } + : {}), + ...(choice.message.audio + ? { + audio: processInputAttachments( + { role: "assistant", audio: choice.message.audio }, + captureAttachments, + audioFormat, + ).audio, + } + : {}), + }, + })); +} + function getCachedMetricFromEndEvent(endEvent: unknown): number | undefined { if (!isObject(endEvent)) { return undefined; diff --git a/js/src/instrumentation/providers/openai-media.ts b/js/src/instrumentation/providers/openai-media.ts index 3e20aec1f..3bb8dda73 100644 --- a/js/src/instrumentation/providers/openai-media.ts +++ b/js/src/instrumentation/providers/openai-media.ts @@ -1,4 +1,10 @@ -import { Attachment, startSpan, withCurrent, type Span } from "../../logger"; +import { + _internalCaptureAttachmentsEnabled, + Attachment, + startSpan, + withCurrent, + type Span, +} from "../../logger"; import { debugLogger } from "../../debug-logger"; import { getCurrentUnixTimestamp } from "../../util"; import { @@ -8,7 +14,6 @@ import { import { convertDataToBlob, getExtensionFromMediaType, - isAutoCaptureAttachmentsEnabled, } from "../../wrappers/attachment-utils"; import { isObject } from "../../../util/index"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; @@ -661,7 +666,7 @@ export function interceptOpenAIMedia( return channel.intercept((target, thisArg, args, additional) => { if (isAutoInstrumentationSuppressed()) return target.apply(thisArg, args); const params = args[0]; - const captureAttachments = isAutoCaptureAttachmentsEnabled(); + const captureAttachments = _internalCaptureAttachmentsEnabled(); let span: Span; try { const { name, spanAttributes, spanInfoMetadata } = buildStartSpanArgs( diff --git a/js/src/instrumentation/providers/openai-span-data.ts b/js/src/instrumentation/providers/openai-span-data.ts index a1414ca84..9e5635dc8 100644 --- a/js/src/instrumentation/providers/openai-span-data.ts +++ b/js/src/instrumentation/providers/openai-span-data.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Attachment } from "../../logger"; +import { _internalCaptureAttachmentsEnabled, Attachment } from "../../logger"; import { - isAutoCaptureAttachmentsEnabled, + omitMediaData, processInputAttachments, } from "../../wrappers/attachment-utils"; import { isObject } from "../../../util/index"; @@ -58,7 +58,7 @@ export function extractOpenAIBatchInput( const input = endpoint === "/v1/chat/completions" ? params.messages : params.input; return { - input: processInputAttachments(input, isAutoCaptureAttachmentsEnabled()), + input: processInputAttachments(input, _internalCaptureAttachmentsEnabled()), metadata: batchMetadata(params), }; } @@ -69,7 +69,10 @@ export function extractOpenAIChatInput(params: Record): { } { const { messages, ...metadata } = params; return { - input: processInputAttachments(messages, isAutoCaptureAttachmentsEnabled()), + input: processInputAttachments( + messages, + _internalCaptureAttachmentsEnabled(), + ), metadata: { ...metadata, provider: "openai" }, }; } @@ -80,7 +83,7 @@ export function extractOpenAIResponsesInput(params: Record): { } { const { input, ...metadata } = params; return { - input: processInputAttachments(input, isAutoCaptureAttachmentsEnabled()), + input: processInputAttachments(input, _internalCaptureAttachmentsEnabled()), metadata: { ...metadata, provider: "openai" }, }; } @@ -98,12 +101,12 @@ export function extractOpenAIResponsesMetadata( /** Convert Responses API base64 image outputs to Braintrust attachments. */ export function processImagesInOutput( output: any, - captureAttachments = isAutoCaptureAttachmentsEnabled(), + captureAttachments = _internalCaptureAttachmentsEnabled(), ): any { if (Array.isArray(output)) { - return output.map((item) => - processImagesInOutput(item, captureAttachments), - ); + return output + .map((item) => processImagesInOutput(item, captureAttachments)) + .filter((item) => item !== undefined); } if ( @@ -113,7 +116,7 @@ export function processImagesInOutput( output.result ) { if (!captureAttachments) { - return { ...output, result: "" }; + return omitMediaData(output, "result"); } const fileExtension = output.output_format || "png"; const contentType = `image/${fileExtension}`; diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts index 43d9cedb0..2002880c3 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts @@ -25,11 +25,18 @@ vi.mock("../../isomorph", async (importOriginal) => { }; }); -vi.mock("../../logger", () => ({ - withCurrent: (_span: unknown, callback: () => unknown) => callback(), - startSpan: (...args: unknown[]) => mockStartSpan(...args), - _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), -})); +vi.mock("../../logger", async () => { + const { default: iso } = await import("../../isomorph"); + return { + _internalCaptureAttachmentsEnabled: () => + iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", + CAPTURE_ATTACHMENTS: Symbol.for("braintrust.captureAttachments"), + BaseAttachment: class {}, + _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), + startSpan: (...args: unknown[]) => mockStartSpan(...args), + withCurrent: (_span: unknown, callback: () => unknown) => callback(), + }; +}); import { isAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; import { registerPiCodingAgentInstrumentation } from "./pi-coding-agent-instrumentation"; diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts index bb1f83b41..481c1f841 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts @@ -2,7 +2,12 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import iso, { type IsoAsyncLocalStorage } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; -import { _internalStartSpan as startBaseSpan, withCurrent } from "../../logger"; +import { + _internalCaptureAttachmentsEnabled, + CAPTURE_ATTACHMENTS, + withCurrent, + _internalStartSpan as startBaseSpan, +} from "../../logger"; import type { Span } from "../../logger"; import { INSTRUMENTATION_NAMES, @@ -363,14 +368,19 @@ async function startPiLlmSpan( ...extractToolMetadata(context.tools), "pi_coding_agent.operation": `agent.${property}`, }; + const captureAttachments = _internalCaptureAttachmentsEnabled(state.span); const span = startBaseSpan( withSpanInstrumentationName( { event: { - input: processInputAttachments(normalizePiContextInput(context)), + input: processInputAttachments( + normalizePiContextInput(context), + captureAttachments, + ), metadata, }, name: getLlmSpanName(model), + [CAPTURE_ATTACHMENTS]: captureAttachments, parent: await state.span.export(), spanAttributes: { type: SpanTypeAttribute.LLM }, }, @@ -535,13 +545,19 @@ async function handlePiAgentEvent( switch (event.type) { case "message_end": if (isPiAssistantMessage(event.message)) { - state.output = extractAssistantOutput(event.message); + state.output = extractAssistantOutput( + event.message, + _internalCaptureAttachmentsEnabled(state.span), + ); } return; case "turn_end": state.turnEnded = true; if (isPiAssistantMessage(event.message)) { - state.output = extractAssistantOutput(event.message); + state.output = extractAssistantOutput( + event.message, + _internalCaptureAttachmentsEnabled(state.span), + ); if (!state.collectedLlmUsageMetrics) { addMetrics(state.metrics, extractUsageMetrics(event.message.usage)); } @@ -582,6 +598,7 @@ async function startPiToolSpan( metadata, }, name: event.toolName || "tool", + [CAPTURE_ATTACHMENTS]: _internalCaptureAttachmentsEnabled(state.span), parent: await state.span.export(), spanAttributes: { type: SpanTypeAttribute.TOOL }, }, @@ -689,7 +706,14 @@ function finishPiLlmSpan( ...(message ? extractAssistantMetadata(message) : {}), }, metrics, - ...(message ? { output: extractAssistantOutput(message) } : {}), + ...(message + ? { + output: extractAssistantOutput( + message, + _internalCaptureAttachmentsEnabled(llmState.span), + ), + } + : {}), }); } finally { llmState.span.end(); @@ -797,14 +821,20 @@ function normalizeToolCall(toolCall: PiToolCall): unknown { }; } -function extractAssistantOutput(message: PiAssistantMessage): unknown { - return processInputAttachments([ - { - finish_reason: normalizeStopReason(message.stopReason), - index: 0, - message: normalizeAssistantMessage(message), - }, - ]); +function extractAssistantOutput( + message: PiAssistantMessage, + captureAttachments: boolean, +): unknown { + return processInputAttachments( + [ + { + finish_reason: normalizeStopReason(message.stopReason), + index: 0, + message: normalizeAssistantMessage(message), + }, + ], + captureAttachments, + ); } function isPiUserMessage( diff --git a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts index 8439682f6..b29a598ce 100644 --- a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts @@ -24,7 +24,9 @@ const { mockWithCurrent, mockNewAsyncLocalStorage, mockStartSpan } = vi.hoisted( vi.mock("../../isomorph", () => ({ default: { - getEnv: vi.fn(), + getEnv: vi.fn((name: string) => + name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, + ), newAsyncLocalStorage: mockNewAsyncLocalStorage, newTracingChannel: vi.fn(), }, @@ -39,6 +41,9 @@ vi.mock("../../logger", async (importOriginal) => { }; }); +vi.mock("../../lru-cache", { spy: true }); + +import { LRUCache } from "../../lru-cache"; import iso from "../../isomorph"; import { Attachment } from "../../logger"; import { isAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; @@ -318,11 +323,18 @@ describe("registerStrandsAgentSDKInstrumentation", () => { }); it.each([ - ["binary objects", new Uint8Array([1, 2, 3])], - ["base64 strings", "AQID"], + ["binary objects", new Uint8Array([1, 2, 3]), true], + ["omitted binary objects", new Uint8Array([1, 2, 3]), false], + ["base64 strings", "AQID", true], + ["omitted base64 strings", "AQID", false], ])( "converts media from %s to one attachment shared by agent and model spans", - async (_description, bytes) => { + async (_description, bytes, captureAttachments) => { + vi.mocked(iso.getEnv).mockImplementation((name) => + name === "BRAINTRUST_CAPTURE_ATTACHMENTS" + ? String(captureAttachments) + : undefined, + ); registerStrandsAgentSDKInstrumentation(); const handlers = handlersByName.get( @@ -368,10 +380,16 @@ describe("registerStrandsAgentSDKInstrumentation", () => { (span) => span.args.name === "Strands model: gpt-4o-mini", ); const rootAttachment = - rootSpan?.args.event.input[0].document.source.bytes; + rootSpan?.args.event.input[0].document.source?.bytes; const modelAttachment = - modelSpan?.args.event.input[0].content[0].document.source.bytes; + modelSpan?.args.event.input[0].content[0].document.source?.bytes; + if (!captureAttachments) { + expect(LRUCache).not.toHaveBeenCalled(); + expect(rootAttachment).toBeUndefined(); + expect(modelAttachment).toBeUndefined(); + return; + } expect(rootAttachment).toBeInstanceOf(Attachment); expect(rootAttachment.reference).toMatchObject({ content_type: "application/pdf", diff --git a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts index 03ea535e3..c3b791078 100644 --- a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts @@ -2,6 +2,7 @@ import { toLoggedError } from "../core"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import { debugLogger } from "../../debug-logger"; import { + _internalCaptureAttachmentsEnabled, Attachment, BaseAttachment, startSpan as startBaseSpan, @@ -15,7 +16,10 @@ import { import { LRUCache } from "../../lru-cache"; import { getCurrentUnixTimestamp } from "../../util"; import { SpanTypeAttribute, isObject } from "../../../util/index"; -import { convertDataToBlob } from "../../wrappers/attachment-utils"; +import { + convertDataToBlob, + omitMediaData, +} from "../../wrappers/attachment-utils"; import { runWithAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; import { strandsAgentSDKChannels } from "./strands-agent-sdk-channels"; import type { @@ -91,10 +95,13 @@ type ActiveChildParents = WeakMap>; const MAX_STRANDS_STRING_ATTACHMENT_CACHE_ENTRIES = 32; -type StrandsAttachmentCache = { - objects: WeakMap>; - strings: LRUCache>; -}; +type StrandsAttachmentCache = + | { captureAttachments: false } + | { + captureAttachments: true; + objects: WeakMap>; + strings: LRUCache>; + }; class StrandsAgentSDKInstrumentationConsumer { private readonly activeChildParents: ActiveChildParents = new WeakMap(); @@ -235,7 +242,7 @@ function startAgentStream( const parentSpan = agent ? getOnlyChildParent(activeChildParents, agent) : undefined; - const attachmentCache = createStrandsAttachmentCache(); + const attachmentCache = createStrandsAttachmentCache(parentSpan); const processedInput = processStrandsInputAttachments(input, attachmentCache); const span = parentSpan ? withCurrent(parentSpan, () => @@ -777,7 +784,8 @@ function finalizeAgentStream( ...(output !== undefined ? { output } : {}), }); state.span.end(); - state.attachmentCache.strings.clear(); + if (state.attachmentCache.captureAttachments) + state.attachmentCache.strings.clear(); } function finalizeMultiAgentStream( @@ -1002,8 +1010,11 @@ const STRANDS_MEDIA_TYPES: Record = { xml: "application/xml", }; -function createStrandsAttachmentCache(): StrandsAttachmentCache { +function createStrandsAttachmentCache(parent?: Span): StrandsAttachmentCache { + if (!_internalCaptureAttachmentsEnabled(parent)) + return { captureAttachments: false }; return { + captureAttachments: true, objects: new WeakMap(), strings: new LRUCache({ max: MAX_STRANDS_STRING_ATTACHMENT_CACHE_ENTRIES, @@ -1130,35 +1141,44 @@ function createStrandsMediaAttachment( return undefined; } - const contentType = STRANDS_MEDIA_TYPES[format.toLowerCase()]; - if (!contentType) { - return undefined; - } - const filename = - mediaKey === "document" && - typeof media.name === "string" && - media.name.length > 0 - ? media.name - : `${mediaKey}.${format.toLowerCase()}`; - const attachment = getOrCreateStrandsAttachment( - source.bytes, - filename, - contentType, - cache, - ); - if (!attachment) { - return undefined; + let attachment: Attachment | undefined; + if (cache.captureAttachments) { + const contentType = STRANDS_MEDIA_TYPES[format.toLowerCase()]; + if (!contentType) { + return undefined; + } + const filename = + mediaKey === "document" && + typeof media.name === "string" && + media.name.length > 0 + ? media.name + : `${mediaKey}.${format.toLowerCase()}`; + attachment = getOrCreateStrandsAttachment( + source.bytes, + filename, + contentType, + cache, + ); + if (!attachment) { + return undefined; + } } - const { type: _type, ...serializedMedia } = media; - const { type: _sourceType, ...serializedSource } = source; + const { type: _type, source: _source, ...serializedMedia } = media; + const serializedSource = cache.captureAttachments + ? omitMediaData(source, "type") + : omitMediaData(source, "type", "bytes"); return { [mediaKey]: { ...serializedMedia, - source: { - ...serializedSource, - bytes: attachment, - }, + ...(serializedSource || attachment + ? { + source: { + ...serializedSource, + ...(attachment ? { bytes: attachment } : {}), + }, + } + : {}), }, }; } @@ -1167,7 +1187,7 @@ function getOrCreateStrandsAttachment( data: unknown, filename: string, contentType: string, - cache: StrandsAttachmentCache, + cache: Extract, ): Attachment | undefined { const key = `${contentType}\0${filename}`; const attachments = diff --git a/js/src/instrumentation/providers/voyageai-instrumentation.test.ts b/js/src/instrumentation/providers/voyageai-instrumentation.test.ts index 2cdb491b4..874b76472 100644 --- a/js/src/instrumentation/providers/voyageai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/voyageai-instrumentation.test.ts @@ -21,6 +21,7 @@ describe("registerVoyageAIInstrumentation", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ + captureAttachments: true, projectName: "voyageai-instrumentation.test.ts", projectId: "test-project-id", }); diff --git a/js/src/logger.ts b/js/src/logger.ts index 8ebaddaa4..a49594be1 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -753,6 +753,28 @@ function normalizeProxyConnUrl(proxyUrl: string): string { : proxyUrl; } +/** @internal */ +export const CAPTURE_ATTACHMENTS = Symbol.for("braintrust.captureAttachments"); + +function captureAttachmentsFromEnv(): boolean { + return ["1", "true"].includes( + iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS")?.trim().toLowerCase() ?? "", + ); +} + +/** + * @internal Resolve automatic attachment capture for a logger or span. Only plain + * fields are read, so parents and global state from other SDK bundles also work. + */ +export function _internalCaptureAttachmentsEnabled(parent?: object): boolean { + // Global state is unset until a platform entrypoint configures the SDK. + const owner = parent ?? (_globalState ? getSpanParentObject() : undefined); + const policy = owner && Reflect.get(owner, CAPTURE_ATTACHMENTS); + return typeof policy === "boolean" + ? policy + : (_globalState?.captureAttachments ?? captureAttachmentsFromEnv()); +} + export class BraintrustState { public id: string; public currentExperiment: Experiment | undefined; @@ -775,6 +797,8 @@ export class BraintrustState { public proxyUrl: string | null = null; public loggedIn: boolean = false; public gitMetadataSettings?: GitMetadataSettings; + /** @internal Default for instrumentation without a logger-specific policy. */ + public captureAttachments?: boolean; public debugLogLevel?: DebugLogLevel; private debugLogLevelConfigured = false; @@ -2886,6 +2910,7 @@ export class Logger implements Exportable { private lastStartTime: number; private lazyId: LazyValue; private calledStartSpan: boolean; + private readonly captureAttachments: boolean | undefined; // For type identification. public kind = "logger" as const; @@ -2896,6 +2921,7 @@ export class Logger implements Exportable { options: { computeMetadataArgs?: ProjectMetadataArgs; linkArgs?: LinkArgs; + captureAttachments?: boolean; } = {}, ) { this.lazyMetadata = lazyMetadata; @@ -2904,6 +2930,7 @@ export class Logger implements Exportable { this.lastStartTime = getCurrentUnixTimestamp(); this.lazyId = new LazyValue(() => this.id); this.calledStartSpan = false; + this.captureAttachments = options.captureAttachments; this.state = state; } @@ -2927,6 +2954,11 @@ export class Logger implements Exportable { return this.state; } + /** @internal */ + public get [CAPTURE_ATTACHMENTS](): boolean { + return this.captureAttachments ?? captureAttachmentsFromEnv(); + } + private parentObjectType() { return SpanObjectTypeV3.PROJECT_LOGS; } @@ -3004,6 +3036,7 @@ export class Logger implements Exportable { private startSpanImpl(args?: StartSpanArgs): Span { return new SpanImpl({ + [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], ...args, // Sometimes `args` gets passed directly into this function, and it contains an undefined value for `state`. // To ensure that we always use this logger's state, we override the `state` argument no matter what. @@ -4976,6 +5009,15 @@ async function computeLoggerMetadata( } export type InitLoggerOptions = FullLoginOptions & { + /** + * Capture inline media as attachments in instrumentation. Explicit booleans override + * BRAINTRUST_CAPTURE_ATTACHMENTS (1 or true); capture is otherwise disabled. + * With setCurrent: false, this logger and its child spans have an independent policy + * and an omitted option uses the environment, not the global logger's setting. + * Current loggers also update the state's default when this option is supplied; + * omitting it preserves that default. Explicitly logged attachments are unaffected. + */ + captureAttachments?: boolean; projectName?: string; projectId?: string; _createInProjectGroup?: string; @@ -5048,11 +5090,18 @@ export function initLogger(options: Readonly = {}): Logger { }, ); + const setCurrent = options.setCurrent ?? true; + if (setCurrent && options.captureAttachments !== undefined) { + state.captureAttachments = options.captureAttachments; + } const ret = new Logger(state, lazyMetadata, { + captureAttachments: + options.captureAttachments ?? + (setCurrent ? state.captureAttachments : undefined), computeMetadataArgs, linkArgs, }); - if (options.setCurrent ?? true) { + if (setCurrent) { state.currentLogger = ret; } return ret; @@ -8006,6 +8055,8 @@ function _resolveSpanIds( * We suggest using one of the various `traced` methods, instead of creating Spans directly. See {@link Span.startSpan} for full details. */ export class SpanImpl implements Span { + /** @internal Snapshot inherited by descendants and delayed instrumentation. */ + public readonly [CAPTURE_ATTACHMENTS]: boolean; private _state: BraintrustState; private isMerge: boolean; @@ -8041,11 +8092,16 @@ export class SpanImpl implements Span { defaultRootType?: SpanType; spanId?: string; propagatedState?: PropagatedState | undefined; + [CAPTURE_ATTACHMENTS]?: boolean; } & Omit & InitialSpanWriteAsMergeArg & InternalSpanContextArg, ) { this._state = args.state; + this[CAPTURE_ATTACHMENTS] = + args[CAPTURE_ATTACHMENTS] ?? + args.state.captureAttachments ?? + captureAttachmentsFromEnv(); this._propagatedState = args.propagatedState; const instrumentationName = getSpanInstrumentationName(args) ?? @@ -8275,6 +8331,7 @@ export class SpanImpl implements Span { : { spanId: this._spanId, rootSpanId: this._rootSpanId }; return new SpanImpl({ state: this._state, + [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], ...args, ...startSpanParentArgs({ state: this._state, @@ -8300,6 +8357,7 @@ export class SpanImpl implements Span { }; return new SpanImpl({ state: this._state, + [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], ...args, ...startSpanParentArgs({ state: this._state, diff --git a/js/src/vendor-sdk-types/openai-common.ts b/js/src/vendor-sdk-types/openai-common.ts index a6ec770de..177f0f025 100644 --- a/js/src/vendor-sdk-types/openai-common.ts +++ b/js/src/vendor-sdk-types/openai-common.ts @@ -22,6 +22,7 @@ export interface OpenAIWithResponse { export interface OpenAIChatCreateParams { messages: unknown; stream?: boolean | null; + audio?: { format: string; [key: string]: unknown } | null; [key: string]: unknown; } diff --git a/js/src/wrappers/ai-sdk/normalize-logged-output.ts b/js/src/wrappers/ai-sdk/normalize-logged-output.ts index 40773f126..d60262fc5 100644 --- a/js/src/wrappers/ai-sdk/normalize-logged-output.ts +++ b/js/src/wrappers/ai-sdk/normalize-logged-output.ts @@ -1,4 +1,5 @@ import type { AISDKResult } from "../../vendor-sdk-types/ai-sdk"; +import { BaseAttachment } from "../../logger"; const REMOVE_NORMALIZED_VALUE = Symbol("braintrust.ai-sdk.remove-normalized"); @@ -21,7 +22,12 @@ function normalizeAISDKLoggedValue( .filter((entry) => entry !== REMOVE_NORMALIZED_VALUE); } - if (!value || typeof value !== "object") { + if ( + !value || + typeof value !== "object" || + value instanceof BaseAttachment || + value instanceof URL + ) { return value; } diff --git a/js/src/wrappers/ai-sdk/telemetry.ts b/js/src/wrappers/ai-sdk/telemetry.ts index 72c860c3d..ef4f328ce 100644 --- a/js/src/wrappers/ai-sdk/telemetry.ts +++ b/js/src/wrappers/ai-sdk/telemetry.ts @@ -365,7 +365,9 @@ export function braintrustAISDKTelemetry(): any { span.log({ ...(shouldRecordOutputs(event) ? { - output: processAISDKOutput(result, DEFAULT_DENY_OUTPUT_PATHS), + output: withCurrent(span, () => + processAISDKOutput(result, DEFAULT_DENY_OUTPUT_PATHS), + ), } : {}), metrics: extractTokenMetrics(result), @@ -464,7 +466,9 @@ export function braintrustAISDKTelemetry(): any { state.span.log({ ...(shouldRecordOutputs(event) ? { - output: finishOutput(result, state.operationName), + output: withCurrent(state.span, () => + finishOutput(result, state.operationName), + ), } : {}), metrics, @@ -547,9 +551,11 @@ export function braintrustAISDKTelemetry(): any { if (shouldRecordInputs(event)) { const callInput = workflowAgentCallInput ?? operationInput(event, operationName); - const { input, outputPromise } = workflowAgent - ? processAISDKWorkflowAgentCallInput(callInput) - : processAISDKCallInput(callInput); + const { input, outputPromise } = withCurrent(span, () => + workflowAgent + ? processAISDKWorkflowAgentCallInput(callInput) + : processAISDKCallInput(callInput), + ); logPayload.input = input; const state = operations.get(operationKey); if (state) { @@ -590,12 +596,15 @@ export function braintrustAISDKTelemetry(): any { const operationName = state?.operationName ?? "generateText"; const callInput = operationInput(event, operationName); const workflowAgent = operationName === "WorkflowAgent.stream"; - const processedInput = + const processInput = () => shouldRecordInputs(event) && workflowAgent ? processAISDKWorkflowAgentModelCallInput(callInput).input : shouldRecordInputs(event) ? processAISDKCallInput(callInput).input : undefined; + const processedInput = state + ? withCurrent(state.span, processInput) + : processInput(); if ( workflowAgent && state?.ownsSpan && @@ -738,7 +747,9 @@ export function braintrustAISDKTelemetry(): any { span.log({ ...(shouldRecordOutputs(event) ? { - output: processAISDKOutput(result, DEFAULT_DENY_OUTPUT_PATHS), + output: withCurrent(span, () => + processAISDKOutput(result, DEFAULT_DENY_OUTPUT_PATHS), + ), } : {}), metrics: extractTokenMetrics(result), diff --git a/js/src/wrappers/attachment-utils.test.ts b/js/src/wrappers/attachment-utils.test.ts index 3994ecb5c..1ba582e27 100644 --- a/js/src/wrappers/attachment-utils.test.ts +++ b/js/src/wrappers/attachment-utils.test.ts @@ -1,9 +1,7 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import iso from "../isomorph"; -import { - isAutoCaptureAttachmentsEnabled, - processInputAttachments, -} from "./attachment-utils"; +import { _internalCaptureAttachmentsEnabled, Attachment } from "../logger"; +import { processInputAttachments } from "./attachment-utils"; const originalGetEnv = iso.getEnv; @@ -11,11 +9,11 @@ afterEach(() => { iso.getEnv = originalGetEnv; }); -describe("isAutoCaptureAttachmentsEnabled", () => { +describe("_internalCaptureAttachmentsEnabled", () => { it.each(["1", "true", " TRUE "])('accepts "%s"', (value) => { iso.getEnv = () => value; - expect(isAutoCaptureAttachmentsEnabled()).toBe(true); + expect(_internalCaptureAttachmentsEnabled()).toBe(true); }); it.each([undefined, "0", "false", "yes", "on", "unexpected"])( @@ -23,7 +21,7 @@ describe("isAutoCaptureAttachmentsEnabled", () => { (value) => { iso.getEnv = () => value; - expect(isAutoCaptureAttachmentsEnabled()).toBe(false); + expect(_internalCaptureAttachmentsEnabled()).toBe(false); }, ); }); @@ -50,7 +48,7 @@ it("omits inline attachment data while preserving remote references", () => { expect(processInputAttachments(input, false)).toEqual([ { type: "image_url", - image_url: { detail: "low", url: "" }, + image_url: { detail: "low" }, }, { type: "image_url", @@ -58,7 +56,85 @@ it("omits inline attachment data while preserving remote references", () => { }, { type: "file", - file: { filename: "document.pdf", file_data: "" }, + file: { filename: "document.pdf" }, }, ]); }); + +it("skips data URL parsing for disabled media across supported input formats", () => { + const data = "data:application/octet-stream;base64,AQID"; + const input = [ + { type: "image_url", image_url: { url: data } }, + { type: "image_base64", imageBase64: data }, + { type: "video_base64", video_base64: data }, + { type: "file", file: { file_data: data } }, + { type: "image", image: data }, + { type: "file", data, mediaType: "application/pdf" }, + ]; + const parse = vi.spyOn(String.prototype, "match"); + let output; + let parseCalls; + try { + output = processInputAttachments(input, false); + parseCalls = parse.mock.calls.length; + } finally { + parse.mockRestore(); + } + expect(parseCalls).toBe(0); + expect(output).toEqual([{ type: "file", mediaType: "application/pdf" }]); + expect(JSON.stringify(output)).not.toContain("AQID"); + expect(input[0].image_url?.url).toBe(data); +}); + +it.each([false, true])( + "applies capture=%s to data URL objects while preserving remote URLs", + (captureAttachments) => { + const data = new URL("data:image/png;base64,AQID"); + const remote = new URL("https://example.com/image.png"); + const input = [ + data, + { type: "image", image: data }, + { type: "file", data, mediaType: "image/png" }, + { type: "image_url", image_url: { url: data, detail: "low" } }, + { type: "file", file: { file_data: data, filename: "image.png" } }, + remote, + { type: "image", image: remote }, + { type: "file", data: remote, mediaType: "image/png" }, + ]; + const decode = vi.spyOn(globalThis, "atob"); + try { + const output = processInputAttachments(input, captureAttachments); + if (captureAttachments) { + expect(output.slice(0, 5)).toEqual([ + expect.any(Attachment), + { type: "image", image: expect.any(Attachment) }, + { + type: "file", + data: expect.any(Attachment), + mediaType: "image/png", + }, + { + type: "image_url", + image_url: { url: expect.any(Attachment), detail: "low" }, + }, + { + type: "file", + file: { file_data: expect.any(Attachment), filename: "image.png" }, + }, + ]); + } else { + expect(decode).not.toHaveBeenCalled(); + expect(output.slice(0, -3)).toEqual([ + { type: "file", mediaType: "image/png" }, + { type: "image_url", image_url: { detail: "low" } }, + { type: "file", file: { filename: "image.png" } }, + ]); + } + expect(output.slice(-3)).toEqual(input.slice(-3)); + expect(JSON.stringify(output)).not.toContain("AQID"); + expect(data.href).toBe("data:image/png;base64,AQID"); + } finally { + decode.mockRestore(); + } + }, +); diff --git a/js/src/wrappers/attachment-utils.ts b/js/src/wrappers/attachment-utils.ts index e18b526d5..eb6123845 100644 --- a/js/src/wrappers/attachment-utils.ts +++ b/js/src/wrappers/attachment-utils.ts @@ -1,13 +1,29 @@ -import { Attachment } from "../logger"; -import iso from "../isomorph"; +import { + Attachment, + BaseAttachment, + _internalCaptureAttachmentsEnabled, +} from "../logger"; -const CAPTURE_ATTACHMENTS_ENV_VAR = "BRAINTRUST_CAPTURE_ATTACHMENTS"; - -export function isAutoCaptureAttachmentsEnabled(): boolean { - const value = iso.getEnv(CAPTURE_ATTACHMENTS_ENV_VAR); - return ( - value !== undefined && ["1", "true"].includes(value.trim().toLowerCase()) - ); +/** Remove media fields without reading their values, dropping type-only blocks. */ +export function omitMediaData( + value: object, + ...fields: string[] +): Record | undefined { + const result: Record = {}; + let hasContent = false; + for (const key of Object.keys(value)) { + if (fields.includes(key)) continue; + const item: unknown = Reflect.get(value, key); + if (item === undefined) continue; + Object.defineProperty(result, key, { + value: item, + enumerable: true, + configurable: true, + writable: true, + }); + if (key !== "type") hasContent = true; + } + return hasContent ? result : undefined; } /** @@ -27,6 +43,8 @@ export function getExtensionFromMediaType(mediaType: string): string { "audio/wav": "wav", "audio/webm": "webm", "audio/ogg": "ogg", + "audio/aac": "aac", + "audio/pcm": "pcm", "video/mp4": "mp4", "video/webm": "webm", "application/pdf": "pdf", @@ -44,6 +62,10 @@ export function getExtensionFromMediaType(mediaType: string): string { */ export function convertDataToBlob(data: any, mediaType: string): Blob | null { try { + if (data instanceof URL) { + if (data.protocol !== "data:") return null; + data = data.href; + } if (typeof data === "string") { // Could be base64, data URL, or regular URL if (data.startsWith("data:")) { @@ -86,11 +108,12 @@ export function convertDataToBlob(data: any, mediaType: string): Blob | null { /** * Process input to extract and convert image/file content parts to Attachments - * Similar to processImagesInOutput in oai_responses.ts - replaces data in-place + * Only pass provider content containers, never arbitrary tool arguments or JSON results. */ export function processInputAttachments( input: any, - captureAttachments = true, + captureAttachments = _internalCaptureAttachmentsEnabled(), + audioOutputFormat?: string, ): any { if (!input) { return input; @@ -125,23 +148,151 @@ export function processInputAttachments( const processNode = (node: any): any => { if (Array.isArray(node)) { - return node.map(processNode); + return node.map(processNode).filter((item) => item !== undefined); } if (!node || typeof node !== "object") { return node; } + if (node instanceof BaseAttachment || node instanceof Date) return node; + // Tool arguments and JSON results are application data, not content parts. + if (node.type === "tool-call") return node; + if (node.type === "tool-result") { + return node.output?.type === "content" && Array.isArray(node.output.value) + ? { + ...node, + output: { ...node.output, value: processNode(node.output.value) }, + } + : node; + } + if (node instanceof URL) { + if (node.protocol !== "data:") return node; + if (!captureAttachments) return undefined; + const mediaType = inferMediaTypeFromDataUrl( + node.href, + "application/octet-stream", + ); + return ( + toAttachment( + node, + mediaType, + `file.${getExtensionFromMediaType(mediaType)}`, + ) ?? node + ); + } + + // Responses API and Agents SDK image/file content parts. + if (node.type === "input_image" || node.type === "input_file") { + const field = + node.type === "input_file" + ? "file_data" in node + ? "file_data" + : "file" + : "image_url" in node + ? "image_url" + : "image"; + const data = node[field]; + if (data instanceof BaseAttachment) return node; + if (typeof data === "string" || data instanceof URL) { + if ( + (data instanceof URL && data.protocol !== "data:") || + /^https?:/i.test(String(data)) + ) + return node; + if (!captureAttachments) return omitMediaData(node, field); + const mediaType = inferMediaTypeFromDataUrl( + String(data), + node.type === "input_image" + ? "image/png" + : "application/octet-stream", + ); + const filename = + node.filename || `file.${getExtensionFromMediaType(mediaType)}`; + const attachment = toAttachment(data, mediaType, filename); + if (attachment) return { ...node, [field]: attachment }; + } + } + + if (node.type === "audio" && typeof node.audio === "string") { + if (!captureAttachments) return omitMediaData(node, "audio"); + const format = node.format || "wav"; + const attachment = toAttachment( + node.audio, + format === "mp3" ? "audio/mpeg" : `audio/${format}`, + `audio.${format}`, + ); + if (attachment) return { ...node, audio: attachment }; + } + + const audioField = node.type === "input_audio" ? "input_audio" : "audio"; + if ( + (node.type === "input_audio" || node.role === "assistant") && + node[audioField] && + typeof node[audioField] === "object" + ) { + const audio = node[audioField]; + if (audio.data instanceof BaseAttachment) return node; + if (!captureAttachments) + return omitMediaData({ + ...node, + [audioField]: omitMediaData(audio, "data"), + }); + const format = + audio.format || + (node.role === "assistant" ? audioOutputFormat : undefined) || + "wav"; + const mediaType = + format === "mp3" + ? "audio/mpeg" + : format === "opus" + ? "audio/ogg" + : format === "pcm16" + ? "audio/pcm" + : `audio/${format}`; + const attachment = toAttachment( + audio.data, + mediaType, + `audio.${getExtensionFromMediaType(mediaType)}`, + ); + if (attachment) + return { ...node, [audioField]: { ...audio, data: attachment } }; + } + + // AI SDK generateText/streamText file parts wrap a GeneratedFile instance. + if ( + node.type === "file" && + node.file && + typeof node.file === "object" && + ("base64" in node.file || "uint8Array" in node.file) + ) { + if (!captureAttachments) return omitMediaData(node, "file"); + const mediaType = node.file.mediaType || "application/octet-stream"; + const attachment = toAttachment( + node.file.base64 ?? node.file.uint8Array, + mediaType, + `file.${getExtensionFromMediaType(mediaType)}`, + ); + if (attachment) return { ...node, file: attachment }; + } + // OpenAI chat image_url content format if ( node.type === "image_url" && node.image_url && typeof node.image_url === "object" && - typeof node.image_url.url === "string" && - node.image_url.url.startsWith("data:") + ((typeof node.image_url.url === "string" && + node.image_url.url.startsWith("data:")) || + (node.image_url.url instanceof URL && + node.image_url.url.protocol === "data:")) ) { + if (!captureAttachments) + return omitMediaData({ + ...node, + image_url: omitMediaData(node.image_url, "url"), + }); const mediaType = inferMediaTypeFromDataUrl( - node.image_url.url, + String(node.image_url.url), "image/png", ); const filename = `image.${getExtensionFromMediaType(mediaType)}`; @@ -179,6 +330,7 @@ export function processInputAttachments( typeof voyageBase64Value === "string" && voyageBase64Value.startsWith("data:") ) { + if (!captureAttachments) return omitMediaData(node, voyageBase64Key); const mediaType = inferMediaTypeFromDataUrl( voyageBase64Value, node.type === "video_base64" ? "video/mp4" : "image/png", @@ -201,11 +353,18 @@ export function processInputAttachments( node.type === "file" && node.file && typeof node.file === "object" && - typeof node.file.file_data === "string" && - node.file.file_data.startsWith("data:") + ((typeof node.file.file_data === "string" && + !/^https?:/i.test(node.file.file_data)) || + (node.file.file_data instanceof URL && + node.file.file_data.protocol === "data:")) ) { + if (!captureAttachments) + return omitMediaData({ + ...node, + file: omitMediaData(node.file, "file_data"), + }); const mediaType = inferMediaTypeFromDataUrl( - node.file.file_data, + String(node.file.file_data), "application/octet-stream", ); const filename = @@ -229,9 +388,20 @@ export function processInputAttachments( // AI SDK image content format if (node.type === "image" && node.image) { + if (node.image instanceof BaseAttachment) { + attachmentIndex++; + return node; + } + if (!captureAttachments) { + return (node.image instanceof URL && node.image.protocol !== "data:") || + (typeof node.image === "string" && /^https?:/i.test(node.image)) + ? node + : omitMediaData(node, "image"); + } let mediaType = "image/png"; - if (typeof node.image === "string" && node.image.startsWith("data:")) { - mediaType = inferMediaTypeFromDataUrl(node.image, mediaType); + const image = node.image instanceof URL ? node.image.href : node.image; + if (typeof image === "string" && image.startsWith("data:")) { + mediaType = inferMediaTypeFromDataUrl(image, mediaType); } else if (node.mediaType) { mediaType = node.mediaType; } @@ -253,8 +423,21 @@ export function processInputAttachments( } } - // AI SDK file content format - if (node.type === "file" && node.data) { + // AI SDK file parts and inline media in tool-result content. + if ( + ["file", "image-data", "file-data", "media"].includes(node.type) && + node.data + ) { + if (node.data instanceof BaseAttachment) { + attachmentIndex++; + return node; + } + if (!captureAttachments) { + return (node.data instanceof URL && node.data.protocol !== "data:") || + (typeof node.data === "string" && /^https?:/i.test(node.data)) + ? node + : omitMediaData(node, "data"); + } const mediaType = node.mediaType || "application/octet-stream"; const filename = node.filename || @@ -275,35 +458,16 @@ export function processInputAttachments( } } - // AI SDK tool output content format - if ( - (node.type === "image-data" || node.type === "file-data") && - node.data - ) { - const mediaType = node.mediaType || "application/octet-stream"; - const filename = - node.filename || - `output_${node.type === "image-data" ? "image" : "file"}_${attachmentIndex}.${getExtensionFromMediaType(mediaType)}`; - const attachment = toAttachment(node.data, mediaType, filename); - - if (attachment) { - attachmentIndex++; - return { - ...node, - data: attachment, - }; - } - } - const processed: Record = {}; for (const [key, value] of Object.entries(node)) { - processed[key] = processNode(value); + const result = processNode(value); + if (result !== undefined) processed[key] = result; } return processed; }; if (Array.isArray(input)) { - return input.map(processNode); + return input.map(processNode).filter((item) => item !== undefined); } return processNode(input); diff --git a/js/src/wrappers/groq.test.ts b/js/src/wrappers/groq.test.ts index d44daf491..5a3c19880 100644 --- a/js/src/wrappers/groq.test.ts +++ b/js/src/wrappers/groq.test.ts @@ -30,6 +30,7 @@ describe("groq wrapper", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ + captureAttachments: true, projectId: "test-project-id", projectName: "groq.test.ts", }); @@ -426,39 +427,54 @@ describe("groq wrapper", () => { }); }); - test("captures speech consumed through a cloned response", async () => { - const wrapped = wrapGroq({ - audio: { - speech: { - create: vi.fn(async (_request: Record) => - Promise.resolve( - new Response(new Uint8Array([1, 2, 3]), { - headers: { "content-type": "audio/wav" }, - }), + test.each([false, true])( + "applies capture=%s to speech consumed through a cloned response", + async (captureAttachments) => { + initLogger({ + projectName: "groq.test.ts", + projectId: "test-project-id", + captureAttachments, + }); + const wrapped = wrapGroq({ + audio: { + speech: { + create: vi.fn(async (_request: Record) => + Promise.resolve( + new Response(new Uint8Array([1, 2, 3]), { + headers: { "content-type": "audio/wav" }, + }), + ), ), - ), + }, }, - }, - }); + }); - const response = await wrapped.audio.speech.create({ - input: "Hello from Braintrust.", - model: "playai-tts", - voice: "Fritz-PlayAI", - }); - expect(new Uint8Array(await response.clone().arrayBuffer())).toEqual( - new Uint8Array([1, 2, 3]), - ); - expect(response.bodyUsed).toBe(false); - - const spans = (await backgroundLogger.drain()) as Record[]; - const speechSpan = spans.find( - (span) => span.span_attributes?.name === "groq.audio.speech.create", - )!; - const attachment = speechSpan.output.content[0].file - .file_data as Attachment; - expect( - new Uint8Array(await (await attachment.data()).arrayBuffer()), - ).toEqual(new Uint8Array([1, 2, 3])); - }); + const response = await wrapped.audio.speech.create({ + input: "Hello from Braintrust.", + model: "playai-tts", + voice: "Fritz-PlayAI", + }); + expect(new Uint8Array(await response.clone().arrayBuffer())).toEqual( + new Uint8Array([1, 2, 3]), + ); + expect(response.bodyUsed).toBe(false); + + const spans = (await backgroundLogger.drain()) as Record[]; + const speechSpan = spans.find( + (span) => span.span_attributes?.name === "groq.audio.speech.create", + )!; + if (!captureAttachments) { + expect(speechSpan.output.content).toEqual([]); + expect(speechSpan.metrics.time_to_first_token).toEqual( + expect.any(Number), + ); + return; + } + const attachment = speechSpan.output.content[0].file + .file_data as Attachment; + expect( + new Uint8Array(await (await attachment.data()).arrayBuffer()), + ).toEqual(new Uint8Array([1, 2, 3])); + }, + ); }); diff --git a/js/tests/provider-wrappers.test.ts b/js/tests/provider-wrappers.test.ts index 37161415c..ee4ce6363 100644 --- a/js/tests/provider-wrappers.test.ts +++ b/js/tests/provider-wrappers.test.ts @@ -74,7 +74,7 @@ describe("provider wrapper", () => { _exportsForTestingOnly.clearTestBackgroundLogger(); }); - test("OpenAI responses image outputs are converted to attachments when enabled", async () => { + test("OpenAI responses image outputs are converted to attachments", async () => { const originalAutoCaptureAttachments = process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; process.env.BRAINTRUST_CAPTURE_ATTACHMENTS = "true"; @@ -157,7 +157,9 @@ describe("provider wrapper", () => { ).toEqual(mockData); const spans = await backgroundLogger.drain(); expect(spans).toHaveLength(1); - expect(spans[0].output[0].result).toBe(""); + expect(spans[0].output).toEqual([ + { type: "image_generation_call", output_format: "png" }, + ]); } finally { client.responses.create = originalCreate; if (originalAutoCaptureAttachments === undefined) { From 8037b723129a98a108b5c600bed7e1d7fd66fbd4 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 16 Oct 2026 10:11:14 +0200 Subject: [PATCH 15/19] build(sdk)!: Migrate to Vite+ and tree-shakeable ESM (#2626) --- .changeset/breezy-lines-draw.md | 10 + .github/dependabot.yml | 4 +- .github/scripts/npm-download-stats.ts | 2 +- .github/workflows/checks.yaml | 31 +- .gitignore | 4 + .prettierignore | 17 - .prettierrc | 4 - AGENTS.md | 2 + dev-packages/seinfeld/package.json | 18 +- dev-packages/seinfeld/src/format/v1.ts | 3 +- .../src/internal/well-known-headers.ts | 10 +- dev-packages/seinfeld/tsconfig.json | 2 +- dev-packages/seinfeld/tsup.config.ts | 14 - dev-packages/seinfeld/vite.config.ts | 23 + e2e/.gitignore | 1 + e2e/README.md | 4 +- e2e/config/pr-comment-scenarios.json | 8 + e2e/helpers/attachment-capture-assertions.ts | 2 +- e2e/helpers/file-snapshot.ts | 2 +- e2e/helpers/mock-braintrust-server.test.ts | 2 +- e2e/helpers/normalize.test.ts | 2 +- e2e/helpers/normalize.ts | 4 +- e2e/helpers/pr-e2e-links.test.ts | 41 +- e2e/helpers/scenario-harness.ts | 27 +- e2e/helpers/span-tree.test.ts | 2 +- e2e/helpers/span-tree.ts | 2 +- e2e/package.json | 5 +- .../scenario.test.ts | 380 +- .../ai-sdk-instrumentation/assertions.ts | 2 +- .../scenario.concurrent-shared-model.test.ts | 2 +- .../ai-sdk-instrumentation/scenario.test.ts | 6 +- .../ai-sdk-otel-export/scenario.test.ts | 2 +- .../assertions.ts | 2 +- .../scenario.test.ts | 134 +- .../anthropic-instrumentation/assertions.ts | 2 +- .../scenario.test.ts | 4 +- .../assertions.ts | 2 +- .../scenario.test.ts | 140 +- .../assertions.ts | 2 +- .../scenario.test.ts | 84 +- ...aude-agent-sdk-v0-auto-hook.span-tree.json | 2 +- ...laude-agent-sdk-v0-auto-hook.span-tree.txt | 2 +- ...ent-sdk-v0-latest-auto-hook.span-tree.json | 2 +- ...gent-sdk-v0-latest-auto-hook.span-tree.txt | 2 +- ...agent-sdk-v0-latest-wrapped.span-tree.json | 2 +- ...-agent-sdk-v0-latest-wrapped.span-tree.txt | 2 +- ...claude-agent-sdk-v0-wrapped.span-tree.json | 2 +- .../claude-agent-sdk-v0-wrapped.span-tree.txt | 2 +- .../assertions.ts | 2 +- .../scenario.test.ts | 96 +- .../assertions.ts | 2 +- .../scenario.test.ts | 56 +- .../worker.ts | 4 + .../assertions.ts | 2 +- .../scenario.test.ts | 60 +- .../worker.ts | 3 + .../assertions.ts | 2 +- .../scenario.test.ts | 56 +- .../src/worker.ts | 4 + .../cohere-instrumentation/assertions.ts | 2 +- .../cohere-instrumentation/scenario.test.ts | 4 +- .../cursor-sdk-instrumentation/assertions.ts | 2 +- .../scenario.test.ts | 88 +- .../scenario.test.ts | 228 +- e2e/scenarios/deno-browser/runner.case.ts | 9 +- e2e/scenarios/deno-browser/scenario.test.ts | 2 +- e2e/scenarios/deno-node/scenario.test.ts | 2 +- .../scenario.test.ts | 4 +- .../eve-instrumentation/scenario.test.ts | 4 +- .../flue-instrumentation/assertions.ts | 4 +- .../flue-instrumentation/scenario.test.ts | 72 +- .../flue-instrumentation/v2-assertions.ts | 4 +- .../genkit-instrumentation/assertions.ts | 2 +- .../genkit-instrumentation/scenario.test.ts | 4 +- e2e/scenarios/git-metadata/scenario.test.ts | 2 +- .../assertions.ts | 2 +- .../scenario.test.ts | 84 +- .../google-adk-instrumentation/assertions.ts | 2 +- .../scenario.test.ts | 88 +- .../assertions.ts | 2 +- .../scenario.test.ts | 5 +- .../assertions.ts | 2 +- .../scenario.test.ts | 96 +- .../groq-instrumentation/assertions.ts | 2 +- .../groq-instrumentation/scenario.test.ts | 4 +- .../huggingface-instrumentation/assertions.ts | 2 +- .../scenario.test.ts | 88 +- .../assertions.ts | 2 +- .../scenario.test.ts | 105 +- .../langchain-anthropic/scenario.test.ts | 2 +- .../assertions.ts | 2 +- .../scenario.test.ts | 2 +- .../scenario.test.ts | 540 +- .../langsmith-instrumentation/assertions.ts | 2 +- .../scenario.test.ts | 4 +- .../mastra-instrumentation/scenario.test.ts | 2 +- .../mistral-instrumentation/assertions.ts | 2 +- .../mistral-instrumentation/scenario.test.ts | 4 +- .../scenario.test.ts | 2 +- .../template/app/api/test/route.ts | 2 +- .../nextjs-instrumentation/scenario.test.ts | 2 +- .../ollama-instrumentation/assertions.ts | 2 +- .../ollama-instrumentation/scenario.test.ts | 6 +- .../assertions.ts | 2 +- .../scenario.test.ts | 5 +- .../assertions.ts | 2 +- .../scenario.test.ts | 54 +- .../assertions.ts | 2 +- .../scenario.test.ts | 84 +- .../openai-instrumentation/assertions.ts | 2 +- .../openai-instrumentation/scenario.impl.mjs | 2 +- .../openai-instrumentation/scenario.test.ts | 4 +- .../assertions.ts | 2 +- .../scenario.test.ts | 84 +- .../openrouter-instrumentation/assertions.ts | 2 +- .../scenario.test.ts | 88 +- .../scenario.test.ts | 2 +- .../scenario.test.ts | 2 +- .../scenario.test.ts | 2 +- .../assertions.ts | 2 +- .../scenario.test.ts | 5 +- .../assertions.ts | 2 +- .../scenario.test.ts | 98 +- .../jest-v29-latest.request-flow.json | 32 +- .../__snapshots__/jest-v29.request-flow.json | 32 +- .../test-framework-evals-jest/jest.config.cjs | 3 +- .../{runner.case.cjs => runner.case.mjs} | 10 +- .../test-framework-evals-jest/scenario.mjs | 8 +- .../scenario.test.ts | 2 +- .../scenario.test.ts | 2 +- .../vitest-v2-latest.span-tree.json | 2 +- .../vitest-v2-latest.span-tree.txt | 2 +- .../__snapshots__/vitest-v2.span-tree.json | 2 +- .../__snapshots__/vitest-v2.span-tree.txt | 2 +- .../vitest-v3-latest.span-tree.json | 2 +- .../vitest-v3-latest.span-tree.txt | 2 +- .../__snapshots__/vitest-v3.span-tree.json | 2 +- .../__snapshots__/vitest-v3.span-tree.txt | 2 +- .../vitest-v4-latest.span-tree.json | 2 +- .../vitest-v4-latest.span-tree.txt | 2 +- .../__snapshots__/vitest-v4.span-tree.json | 2 +- .../__snapshots__/vitest-v4.span-tree.txt | 2 +- .../vitest-v5-latest.span-tree.json | 96 + .../vitest-v5-latest.span-tree.txt | 73 + .../__snapshots__/vitest-v5.span-tree.json | 96 + .../__snapshots__/vitest-v5.span-tree.txt | 73 + .../test-framework-evals-vitest/package.json | 8 +- .../pnpm-lock.yaml | 130 + .../runner.case.ts | 16 +- .../scenario.test.ts | 8 +- .../scenario.vitest-v5.ts | 48 + .../vitest.runner-evals-reporter.config.mts | 11 +- .../vitest.runner-v5.config.mts | 13 + .../scenario.test.ts | 2 +- .../__snapshots__/request-flow.json | 4 +- .../__snapshots__/span-tree.json | 2 +- .../__snapshots__/span-tree.txt | 2 +- .../trace-primitives-basic/scenario.test.ts | 2 +- .../typesafe-instrumentation/assertions.ts | 2 +- .../typesafe-instrumentation/scenario.test.ts | 4 +- .../voyageai-instrumentation/assertions.ts | 2 +- .../voyageai-instrumentation/scenario.test.ts | 4 +- .../workflow-eval-batch/scenario.test.ts | 2 +- .../workflow-eval-webhook/scenario.test.ts | 2 +- e2e/scripts/build-pr-e2e-links-comment.mjs | 56 +- e2e/scripts/run-e2e-tests.mjs | 3 +- e2e/scripts/run-record-tests.mjs | 4 +- e2e/{vitest.config.mts => vite.config.ts} | 8 +- integrations/browser-js/package.json | 18 +- integrations/browser-js/tsup.config.ts | 14 - integrations/browser-js/turbo.json | 10 - integrations/browser-js/vite.config.ts | 24 + integrations/deepseek-harness/.gitignore | 1 + integrations/deepseek-harness/package.json | 22 +- .../deepseek-harness/src/index.test.ts | 2 +- integrations/deepseek-harness/src/index.ts | 2 +- integrations/deepseek-harness/tsconfig.json | 6 +- integrations/deepseek-harness/tsup.config.ts | 10 - integrations/deepseek-harness/vite.config.ts | 24 + integrations/otel-js/.gitignore | 1 + integrations/otel-js/CLAUDE.md | 8 +- integrations/otel-js/Dockerfile.test | 15 +- integrations/otel-js/otel-v1/package.json | 23 +- integrations/otel-js/otel-v1/pnpm-lock.yaml | 2751 ++--- .../otel-js/otel-v1/tests/otel-v1.test.ts | 2 +- .../{vitest.config.ts => vite.config.ts} | 2 +- integrations/otel-js/otel-v2/package.json | 23 +- integrations/otel-js/otel-v2/pnpm-lock.yaml | 2752 ++--- .../otel-js/otel-v2/tests/otel-v2.test.ts | 2 +- .../{vitest.config.ts => vite.config.ts} | 2 +- integrations/otel-js/package.json | 20 +- integrations/otel-js/run-tests.sh | 2 +- .../otel-js/smoke/scenarios/otel-v1/Makefile | 4 +- .../scenarios/otel-v1/tests/basic.test.ts | 2 +- .../scenarios/otel-v1/tests/filtering.test.ts | 2 +- integrations/otel-js/src/exporter.test.ts | 2 +- integrations/otel-js/src/id-gen.test.ts | 2 +- integrations/otel-js/src/index.ts | 4 +- integrations/otel-js/src/otel-compat.test.ts | 2 +- integrations/otel-js/src/otel.test.ts | 10 +- .../otel-js/src/span-customizers.test.ts | 2 +- integrations/otel-js/tests/utils.ts | 1 + integrations/otel-js/tsconfig.test.json | 1 + integrations/otel-js/tsup.config.ts | 26 - integrations/otel-js/vite.config.ts | 39 + integrations/otel-js/vitest.config.ts | 18 - integrations/templates-nunjucks/.gitignore | 1 + integrations/templates-nunjucks/package.json | 23 +- .../smoke/scenarios/jest/Makefile | 6 +- .../smoke/scenarios/jest/package.json | 6 +- .../scenarios/jest/tests/jest-basic.test.js | 8 +- .../smoke/scenarios/nextjs/Makefile | 4 +- .../smoke/scenarios/node-esm/Makefile | 4 +- .../scenarios/node-esm/tests/basic.test.ts | 2 +- .../templates-nunjucks/src/index.test.ts | 2 +- integrations/templates-nunjucks/tsconfig.json | 2 +- .../templates-nunjucks/tsup.config.ts | 10 - .../templates-nunjucks/vite.config.ts | 28 + .../templates-nunjucks/vitest.config.ts | 8 - integrations/temporal-js/.gitignore | 1 + .../examples/temporal-ai-sdk/mise.toml | 2 +- .../examples/temporal-cjs/mise.toml | 2 +- .../examples/temporal-esm/mise.toml | 2 +- integrations/temporal-js/package.json | 27 +- integrations/temporal-js/src/temporal.test.ts | 2 +- integrations/temporal-js/tsup.config.ts | 19 - integrations/temporal-js/vite.config.ts | 30 + integrations/temporal-js/vitest.config.ts | 8 - js/.gitignore | 1 + js/NOTICE | 5 + js/eslint.config.ts | 230 - js/licenses/async/LICENSE | 19 + js/package.json | 201 +- js/scripts/benchmark-imports.mjs | 85 + js/smoke/Makefile | 12 +- js/smoke/README.md | 2 +- .../Makefile | 2 +- .../tests/node-esm-import.test.mjs | 5 +- .../cloudflare-vite-hono-vite-dev/Makefile | 2 +- .../tests/vite-dev.test.mjs | 2 +- .../Makefile | 2 +- .../tests/worker.test.mjs | 5 +- .../cloudflare-worker-browser-compat/Makefile | 2 +- .../tests/worker.test.mjs | 5 +- .../Makefile | 2 +- .../tests/worker.test.mjs | 5 +- .../cloudflare-worker-node-compat/Makefile | 2 +- .../tests/worker.test.mjs | 5 +- .../cloudflare-worker-node-no-compat/Makefile | 2 +- .../tests/worker.test.mjs | 5 +- .../scenarios/playwright-browser/Makefile | 2 +- .../playwright-browser/tests/browser.test.ts | 2 +- js/smoke/shared/README.md | 9 +- js/smoke/shared/package.json | 16 +- js/smoke/shared/pnpm-lock.yaml | 1887 ++- js/smoke/shared/src/suites/basic-logging.ts | 16 +- js/smoke/shared/src/suites/eval-smoke.ts | 129 +- .../shared/src/suites/import-verification.ts | 59 +- .../shared/src/suites/prompt-templating.ts | 6 +- js/smoke/shared/tsup.config.ts | 12 - js/smoke/shared/vite.config.ts | 10 + js/src/agent-assertions.test.ts | 2 +- js/src/agent-assertions.ts | 2 +- js/src/attachment-capture.test.ts | 21 +- js/src/auto-instrumentations/bundler/next.ts | 5 +- .../bundler/plugin.test.ts | 2 +- .../auto-instrumentations/bundler/plugin.ts | 245 +- .../configs/anthropic.ts | 57 +- .../configs/bedrock-runtime.test.ts | 2 +- .../configs/cloudflare-think.test.ts | 2 +- .../configs/pi-coding-agent.test.ts | 2 +- .../configs/strands-agent-sdk.test.ts | 2 +- js/src/auto-instrumentations/hook.mts | 18 +- .../import-in-the-middle/package.json | 4 + .../auto-instrumentations/loader/cjs-patch.ts | 4 +- .../loader/mastra-observability-patch.test.ts | 2 +- .../orchestrion-js/transforms.test.ts | 2 +- .../require-in-the-middle/package.json | 4 + js/src/auto.ts | 6 + js/src/background-logger.test.ts | 4 +- js/src/browser/config.ts | 64 +- js/src/browser/index.ts | 4 - js/src/browser/isomorph.ts | 10 + js/src/browser/platform.ts | 49 + js/src/debug-logger.test.ts | 19 +- js/src/debug-logger.ts | 14 +- js/src/edge-light/config.ts | 50 +- js/src/edge-light/index.ts | 4 - js/src/edge-light/isomorph.ts | 10 + js/src/edge-light/platform.ts | 43 + js/src/edge-runtime-bootstrap.test.ts | 18 +- js/src/eval-parameters.ts | 2 +- js/src/exports.ts | 21 +- js/src/framework.test.ts | 6 +- js/src/framework.ts | 50 +- js/src/framework2.test.ts | 11 +- js/src/framework2.ts | 40 +- js/src/functions/invoke.test.ts | 5 +- js/src/functions/invoke.ts | 4 +- js/src/functions/stream.test.ts | 2 +- js/src/git-command.test.ts | 2 +- js/src/git-command.ts | 2 +- js/src/gitutil.ts | 6 +- js/src/global-instrumentation-hooks.test.ts | 2 +- js/src/id-gen.test.ts | 2 +- js/src/id-gen.ts | 2 +- js/src/imports.test.ts | 2 +- .../auto-instrumentation-suppression.test.ts | 2 +- .../auto-instrumentation-suppression.ts | 2 +- js/src/instrumentation/config.ts | 41 - .../core/channel-definitions.test.ts | 2 +- .../core/channel-definitions.ts | 27 +- .../core/channel-tracing-utils.test.ts | 2 +- .../core/channel-tracing-utils.ts | 2 +- .../core/channel-tracing.test.ts | 6 +- .../instrumentation/core/channel-tracing.ts | 13 +- js/src/instrumentation/core/logging.test.ts | 2 +- .../core/stream-patcher.test.ts | 2 +- js/src/instrumentation/index.test.ts | 2 +- js/src/instrumentation/index.ts | 2 +- .../instrumentation-consumers.test.ts | 28 +- .../instrumentation-consumers.ts | 239 +- .../providers/ai-sdk-channels.ts | 520 +- .../ai-sdk-instrumentation.streaming.test.ts | 9 +- .../providers/ai-sdk-instrumentation.test.ts | 10 +- .../providers/ai-sdk-instrumentation.ts | 8 +- .../providers/ai-sdk-v7-telemetry.test.ts | 13 +- .../providers/anthropic-channels.ts | 123 +- .../anthropic-instrumentation.test.ts | 10 +- .../providers/anthropic-instrumentation.ts | 9 +- ...anthropic-sessions-instrumentation.test.ts | 15 +- .../providers/bedrock-runtime-channels.ts | 98 +- .../bedrock-runtime-instrumentation.test.ts | 14 +- .../bedrock-runtime-instrumentation.ts | 2 +- .../providers/claude-agent-sdk-channels.ts | 39 +- ...gent-sdk-instrumentation.streaming.test.ts | 16 +- .../claude-agent-sdk-instrumentation.test.ts | 17 +- .../claude-agent-sdk-instrumentation.ts | 4 +- .../claude-agent-sdk-local-tool-context.ts | 2 +- .../claude-agent-sdk-local-tool-spans.ts | 5 +- .../providers/cloudflare-agents-channels.ts | 39 +- .../cloudflare-agents-instrumentation.test.ts | 10 +- .../cloudflare-agents-instrumentation.ts | 6 +- .../providers/cloudflare-ai-chat-channels.ts | 62 +- .../cloudflare-ai-chat-consumer.test.ts | 15 +- .../providers/cloudflare-ai-chat-consumer.ts | 9 +- .../providers/cloudflare-think-channels.ts | 39 +- .../cloudflare-think-context.test.ts | 21 + .../providers/cloudflare-think-context.ts | 21 +- .../cloudflare-think-instrumentation.test.ts | 16 +- .../cloudflare-think-instrumentation.ts | 6 +- .../providers/cohere-channels.ts | 92 +- .../providers/cohere-instrumentation.test.ts | 2 +- .../providers/cursor-sdk-channels.ts | 105 +- .../cursor-sdk-instrumentation.test.ts | 15 +- .../providers/cursor-sdk-instrumentation.ts | 6 +- .../providers/elevenlabs-channels.ts | 97 +- .../elevenlabs-instrumentation.test.ts | 14 +- .../providers/elevenlabs-instrumentation.ts | 2 +- .../providers/eve-instrumentation.test.ts | 5 +- .../providers/eve-instrumentation.ts | 4 +- .../providers/flue-instrumentation.test.ts | 11 +- .../providers/flue-instrumentation.ts | 4 +- .../providers/genkit-channels.ts | 167 +- .../providers/genkit-instrumentation.test.ts | 12 +- .../providers/genkit-instrumentation.ts | 9 +- .../providers/github-copilot-channels.ts | 80 +- .../github-copilot-instrumentation.test.ts | 2 +- .../github-copilot-instrumentation.ts | 6 +- .../providers/google-adk-channels.ts | 85 +- .../google-adk-instrumentation.test.ts | 15 +- .../providers/google-adk-instrumentation.ts | 9 +- .../providers/google-genai-channels.ts | 182 +- .../google-genai-embedding-usage.test.ts | 12 +- .../google-genai-instrumentation.test.ts | 19 +- .../providers/google-genai-instrumentation.ts | 7 +- .../google-generative-ai-channels.ts | 111 +- ...ogle-generative-ai-instrumentation.test.ts | 5 +- .../google-generative-ai-instrumentation.ts | 4 +- .../providers/groq-channels.ts | 131 +- .../providers/groq-instrumentation.test.ts | 2 +- .../providers/groq-instrumentation.ts | 6 +- .../providers/harness-agent-context.test.ts | 4 +- .../providers/huggingface-channels.ts | 131 +- .../huggingface-instrumentation.test.ts | 2 +- .../providers/huggingface-instrumentation.ts | 2 +- .../huggingface-transformers-channels.ts | 62 +- ...gface-transformers-instrumentation.test.ts | 2 +- ...uggingface-transformers-instrumentation.ts | 12 +- .../providers/instrumentation-names.test.ts | 2 +- .../providers/langchain-channels.ts | 60 +- .../langchain-instrumentation.test.ts | 2 +- .../providers/langchain-instrumentation.ts | 5 +- .../providers/langgraph-sdk-channels.ts | 52 +- .../langgraph-sdk-instrumentation.test.ts | 14 +- .../langgraph-sdk-instrumentation.ts | 2 +- .../providers/langsmith-channels.ts | 81 +- .../langsmith-instrumentation.test.ts | 12 +- .../providers/langsmith-instrumentation.ts | 14 +- .../providers/mistral-channels.ts | 269 +- .../providers/mistral-instrumentation.test.ts | 2 +- .../providers/ollama-channels.ts | 77 +- .../providers/ollama-instrumentation.test.ts | 11 +- .../providers/ollama-instrumentation.ts | 7 +- .../openai-agents-api-instrumentation.test.ts | 15 +- .../openai-agents-api-instrumentation.ts | 2 +- .../providers/openai-agents-channels.ts | 86 +- .../openai-agents-instrumentation.test.ts | 12 +- .../openai-agents-trace-processor.ts | 11 +- .../openai-batch-instrumentation.test.ts | 10 +- .../providers/openai-batch-instrumentation.ts | 2 +- .../providers/openai-channels.ts | 455 +- .../providers/openai-codex-channels.ts | 60 +- .../openai-codex-instrumentation.test.ts | 15 +- .../providers/openai-codex-instrumentation.ts | 6 +- .../providers/openai-instrumentation.test.ts | 4 +- .../providers/openai-instrumentation.ts | 2 +- .../providers/openai-media.test.ts | 7 +- .../instrumentation/providers/openai-media.ts | 18 +- .../providers/openai-span-data.ts | 7 +- .../providers/openrouter-agent-channels.ts | 90 +- .../openrouter-agent-instrumentation.test.ts | 7 +- .../openrouter-agent-instrumentation.ts | 4 +- .../providers/openrouter-channels.ts | 182 +- .../openrouter-instrumentation.test.ts | 7 +- .../providers/openrouter-instrumentation.ts | 4 +- .../providers/pi-coding-agent-channels.ts | 39 +- .../providers/pi-coding-agent-context.test.ts | 12 +- .../pi-coding-agent-instrumentation.test.ts | 17 +- .../pi-coding-agent-instrumentation.ts | 6 +- .../providers/strands-agent-sdk-channels.ts | 93 +- .../strands-agent-sdk-instrumentation.test.ts | 17 +- .../strands-agent-sdk-instrumentation.ts | 4 +- .../providers/typesafe-channels.ts | 39 +- .../providers/typesafe-instrumentation.ts | 4 +- .../providers/voyageai-channels.ts | 108 +- .../voyageai-instrumentation.test.ts | 14 +- .../providers/voyageai-instrumentation.ts | 4 +- js/src/instrumentation/registrations/aisdk.ts | 9 + .../registrations/anthropic.ts | 9 + .../registrations/awsBedrockRuntime.ts | 9 + .../registrations/claudeAgentSDK.ts | 9 + .../registrations/cloudflareAIChat.ts | 9 + .../registrations/cloudflareAgents.ts | 9 + .../registrations/cloudflareThink.ts | 9 + .../instrumentation/registrations/cohere.ts | 9 + .../registrations/cursorSDK.ts | 9 + .../registrations/elevenlabs.ts | 9 + .../instrumentation/registrations/genkit.ts | 9 + .../registrations/gitHubCopilot.ts | 9 + .../registrations/googleADK.ts | 9 + .../registrations/googleGenAI.ts | 9 + .../registrations/googleGenerativeAI.ts | 9 + js/src/instrumentation/registrations/groq.ts | 9 + .../registrations/huggingface.ts | 11 + .../registrations/langchain.ts | 9 + .../registrations/langgraphSDK.ts | 9 + .../registrations/langsmith.ts | 11 + .../instrumentation/registrations/mastra.ts | 9 + .../instrumentation/registrations/mistral.ts | 9 + .../instrumentation/registrations/ollama.ts | 9 + .../registrations/openAIAgents.ts | 9 + .../instrumentation/registrations/openai.ts | 9 + .../registrations/openaiCodexSDK.ts | 9 + .../registrations/openrouter.ts | 9 + .../registrations/openrouterAgent.ts | 9 + .../registrations/piCodingAgent.ts | 9 + .../registrations/strandsAgentSDK.ts | 9 + .../instrumentation/registrations/typesafe.ts | 9 + .../instrumentation/registrations/voyageai.ts | 9 + js/src/instrumentation/registry.test.ts | 320 +- js/src/instrumentation/registry.ts | 178 +- js/src/isomorph-base.ts | 120 + js/src/isomorph.ts | 127 +- js/src/logger-caching.test.ts | 6 +- js/src/logger-json-attachment.test.ts | 9 +- js/src/logger-misc.test.ts | 15 +- js/src/logger.test.ts | 48 +- js/src/logger.ts | 10285 +--------------- js/src/logger/core.ts | 6703 ++++++++++ js/src/logger/datasets.ts | 1022 ++ js/src/logger/experiments.ts | 849 ++ js/src/logger/lifecycle.test.ts | 30 + js/src/logger/objects.ts | 207 + js/src/logger/prompts.ts | 1528 +++ js/src/logger/testing.ts | 138 + js/src/lru-cache.test.ts | 2 +- js/src/lru-cache.ts | 2 +- js/src/masking.test.ts | 13 +- .../node/apply-auto-instrumentation-entry.ts | 1 + js/src/node/apply-auto-instrumentation.ts | 21 +- js/src/node/config.test.ts | 4 +- js/src/node/config.ts | 196 +- js/src/node/index.ts | 4 - js/src/node/isomorph.ts | 10 + js/src/node/platform.ts | 169 + js/src/object-fetcher.test.ts | 9 +- js/src/openai-agents-api.ts | 6 +- js/src/openai-batch.ts | 6 +- js/src/openai-promise-utils.test.ts | 2 +- js/src/package-exports.test.ts | 6 +- js/src/parameters.test.ts | 4 +- js/src/preprocessors/registry.test.ts | 2 +- js/src/prompt-cache/cache-config.ts | 2 +- js/src/prompt-cache/cache-mode.test.ts | 146 +- js/src/prompt-cache/disk-cache.test.ts | 2 +- js/src/prompt-cache/disk-cache.ts | 2 +- js/src/prompt-cache/parameters-cache.test.ts | 4 +- js/src/prompt-cache/parameters-cache.ts | 2 +- js/src/prompt-cache/prompt-cache.test.ts | 4 +- js/src/prompt-cache/prompt-cache.ts | 2 +- js/src/prompt-types.test.ts | 4 +- js/src/prompt.test.ts | 4 +- js/src/propagation.test.ts | 13 +- js/src/public-types.test.ts | 11 +- js/src/queue.test.ts | 2 +- js/src/queue.ts | 2 +- js/src/reporters/progress.ts | 8 +- js/src/sandbox.ts | 2 +- js/src/sdk-contracts.test.ts | 166 +- js/src/sdk-schemas.ts | 1187 +- js/src/sdk-types.ts | 69 + js/src/span-cache.test.ts | 4 +- js/src/span-cache.ts | 2 +- js/src/span-customizer.test.ts | 8 +- js/src/span-origin.test.ts | 4 +- js/src/span-origin.ts | 4 +- js/src/stackutil.ts | 11 +- js/src/template/mustache-utils.ts | 2 +- js/src/template/registry.ts | 23 +- js/src/template/renderer.test.ts | 2 +- js/src/trace.test.ts | 9 +- js/src/trace.ts | 17 +- js/src/util.test.ts | 2 +- js/src/util.ts | 7 +- js/src/vendor-sdk-types/langchain.ts | 2 +- js/src/workerd/config.ts | 50 +- js/src/workerd/index.ts | 4 - js/src/workerd/isomorph.ts | 10 + js/src/workerd/platform.ts | 43 + js/src/workflow-eval.test.ts | 2 +- js/src/workflow-eval.ts | 25 +- js/src/wrappers/ai-sdk-evaluate.test.ts | 21 +- .../ai-sdk/ai-sdk-generate-image.test.ts | 14 +- js/src/wrappers/ai-sdk/ai-sdk.test.ts | 9 +- js/src/wrappers/ai-sdk/ai-sdk.ts | 3 + .../wrappers/ai-sdk/harness-agent-context.ts | 9 +- .../ai-sdk/normalize-logged-output.ts | 2 +- js/src/wrappers/ai-sdk/telemetry.ts | 2 +- .../wrappers/ai-sdk/workflow-agent-context.ts | 2 +- js/src/wrappers/anthropic-no-install.test.ts | 2 +- .../wrappers/anthropic-session-collector.ts | 2 + js/src/wrappers/anthropic-tokens-util.test.ts | 2 +- js/src/wrappers/anthropic.ts | 2 + js/src/wrappers/attachment-utils.test.ts | 6 +- js/src/wrappers/attachment-utils.ts | 2 +- js/src/wrappers/bedrock-runtime.test.ts | 5 +- js/src/wrappers/bedrock-runtime.ts | 2 + .../claude-agent-sdk/claude-agent-sdk.ts | 2 + js/src/wrappers/cloudflare-agent.test.ts | 4 +- js/src/wrappers/cloudflare-agent.ts | 2 + js/src/wrappers/cloudflare-ai-chat.test.ts | 4 +- js/src/wrappers/cloudflare-ai-chat.ts | 2 + js/src/wrappers/cloudflare-think.test.ts | 4 +- js/src/wrappers/cloudflare-think.ts | 2 + js/src/wrappers/cohere.test.ts | 5 +- js/src/wrappers/cohere.ts | 2 + js/src/wrappers/cursor-sdk.test.ts | 4 +- js/src/wrappers/cursor-sdk.ts | 2 + js/src/wrappers/elevenlabs.ts | 2 + js/src/wrappers/genkit.test.ts | 4 +- js/src/wrappers/genkit.ts | 2 + js/src/wrappers/github-copilot.ts | 2 + js/src/wrappers/google-adk.test.ts | 4 +- js/src/wrappers/google-adk.ts | 2 + js/src/wrappers/google-genai.test.ts | 4 +- js/src/wrappers/google-genai.ts | 2 + js/src/wrappers/google-generative-ai.ts | 2 + js/src/wrappers/groq.test.ts | 5 +- js/src/wrappers/groq.ts | 2 + .../wrappers/huggingface-transformers.test.ts | 9 +- js/src/wrappers/huggingface-transformers.ts | 2 + js/src/wrappers/huggingface.test.ts | 9 +- js/src/wrappers/huggingface.ts | 2 + .../langchain/callback-handler.test.ts | 2 +- js/src/wrappers/langchain/callback-handler.ts | 11 +- js/src/wrappers/langgraph-sdk.ts | 2 + js/src/wrappers/langsmith.test.ts | 4 +- js/src/wrappers/langsmith.ts | 4 + js/src/wrappers/mastra.test.ts | 5 +- js/src/wrappers/mastra.ts | 4 +- js/src/wrappers/mistral.ts | 2 + .../node-test/node-test-example.test.ts | 4 +- .../wrappers/node-test/node-test-span.test.ts | 4 +- js/src/wrappers/node-test/node-test.test.ts | 9 +- js/src/wrappers/node-test/suite.ts | 2 +- js/src/wrappers/node-test/test-helpers.ts | 8 +- js/src/wrappers/oai.ts | 5 +- js/src/wrappers/oai_responses.ts | 2 +- js/src/wrappers/ollama.test.ts | 2 +- js/src/wrappers/ollama.ts | 2 + js/src/wrappers/openai-codex.ts | 2 + js/src/wrappers/openrouter-agent.test.ts | 2 +- js/src/wrappers/openrouter-agent.ts | 2 + js/src/wrappers/openrouter.test.ts | 5 +- js/src/wrappers/openrouter.ts | 2 + js/src/wrappers/pi-coding-agent.test.ts | 4 +- js/src/wrappers/pi-coding-agent.ts | 2 + js/src/wrappers/shared/flush.ts | 2 +- js/src/wrappers/shared/format.ts | 2 +- js/src/wrappers/shared/logging.ts | 4 +- js/src/wrappers/shared/scorers.test.ts | 4 +- js/src/wrappers/shared/scorers.ts | 2 +- js/src/wrappers/shared/traced-eval.ts | 3 +- js/src/wrappers/strands-agent-sdk.test.ts | 4 +- js/src/wrappers/strands-agent-sdk.ts | 2 + js/src/wrappers/test_oai_attachments.test.ts | 4 +- js/src/wrappers/typesafe.test.ts | 5 +- js/src/wrappers/typesafe.ts | 2 + js/src/wrappers/vitest-evals/reporter.test.ts | 15 +- js/src/wrappers/vitest-evals/reporter.ts | 3 +- js/src/wrappers/vitest/context-manager.ts | 5 +- js/src/wrappers/vitest/expect-wrapper.ts | 10 +- js/src/wrappers/vitest/index.ts | 5 +- js/src/wrappers/vitest/package.json | 5 +- js/src/wrappers/vitest/types.ts | 12 +- js/src/wrappers/vitest/vite.config.ts | 16 + .../vitest/vitest-wrapper-example.test.ts | 8 +- .../vitest/vitest-wrapper-span.test.ts | 8 +- js/src/wrappers/vitest/vitest-wrapper.test.ts | 16 +- js/src/wrappers/vitest/vitest.config.js | 8 - js/src/wrappers/vitest/wrapper.ts | 2 +- js/src/wrappers/voyageai.test.ts | 5 +- js/src/wrappers/voyageai.ts | 2 + js/src/zod/utils.ts | 4 +- .../api-compatibility.test.ts | 388 +- .../bundled-runtime.test.ts | 9 +- .../import-require-in-the-middle.test.ts | 2 +- .../auto-instrumentations/loader-hook.test.ts | 7 +- .../auto-instrumentations/next-config.test.ts | 9 +- .../orchestrion-js-upstream.test.ts | 2 +- js/tests/auto-instrumentations/test-bundle.ts | 2 +- .../transformation.test.ts | 4 +- js/tests/provider-wrappers.test.ts | 2 +- js/tsconfig.json | 15 +- js/tsconfig.test.json | 11 +- js/tsup.config.ts | 112 - js/util/json_util.test.ts | 2 +- js/util/json_util.ts | 2 +- js/util/merge_row_batch.test.ts | 2 +- js/util/object_util.test.ts | 2 +- js/util/object_util.ts | 6 +- js/util/span_identifier_v1.ts | 8 +- js/util/span_identifier_v2.ts | 9 +- js/util/span_identifier_v4.test.ts | 2 +- js/util/string_util.test.ts | 2 +- js/util/string_util.ts | 10 +- js/util/zod_util.test.ts | 2 +- js/util/zod_util.ts | 5 +- js/vite.config.ts | 203 + js/vitest.config.js | 65 - knip.jsonc | 25 +- package.json | 81 +- pnpm-lock.yaml | 4003 ++++-- pnpm-workspace.yaml | 47 + turbo.json | 103 - vite.config.ts | 167 + 667 files changed, 26967 insertions(+), 21557 deletions(-) create mode 100644 .changeset/breezy-lines-draw.md delete mode 100644 .prettierignore delete mode 100644 .prettierrc delete mode 100644 dev-packages/seinfeld/tsup.config.ts create mode 100644 dev-packages/seinfeld/vite.config.ts create mode 100644 e2e/.gitignore rename e2e/scenarios/test-framework-evals-jest/{runner.case.cjs => runner.case.mjs} (97%) create mode 100644 e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5-latest.span-tree.json create mode 100644 e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5-latest.span-tree.txt create mode 100644 e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5.span-tree.json create mode 100644 e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5.span-tree.txt create mode 100644 e2e/scenarios/test-framework-evals-vitest/scenario.vitest-v5.ts create mode 100644 e2e/scenarios/test-framework-evals-vitest/vitest.runner-v5.config.mts rename e2e/{vitest.config.mts => vite.config.ts} (66%) delete mode 100644 integrations/browser-js/tsup.config.ts delete mode 100644 integrations/browser-js/turbo.json create mode 100644 integrations/browser-js/vite.config.ts create mode 100644 integrations/deepseek-harness/.gitignore delete mode 100644 integrations/deepseek-harness/tsup.config.ts create mode 100644 integrations/deepseek-harness/vite.config.ts create mode 100644 integrations/otel-js/.gitignore rename integrations/otel-js/otel-v1/{vitest.config.ts => vite.config.ts} (93%) rename integrations/otel-js/otel-v2/{vitest.config.ts => vite.config.ts} (93%) delete mode 100644 integrations/otel-js/tsup.config.ts create mode 100644 integrations/otel-js/vite.config.ts delete mode 100644 integrations/otel-js/vitest.config.ts create mode 100644 integrations/templates-nunjucks/.gitignore delete mode 100644 integrations/templates-nunjucks/tsup.config.ts create mode 100644 integrations/templates-nunjucks/vite.config.ts delete mode 100644 integrations/templates-nunjucks/vitest.config.ts create mode 100644 integrations/temporal-js/.gitignore delete mode 100644 integrations/temporal-js/tsup.config.ts create mode 100644 integrations/temporal-js/vite.config.ts delete mode 100644 integrations/temporal-js/vitest.config.ts delete mode 100644 js/eslint.config.ts create mode 100644 js/licenses/async/LICENSE create mode 100644 js/scripts/benchmark-imports.mjs delete mode 100644 js/smoke/shared/tsup.config.ts create mode 100644 js/smoke/shared/vite.config.ts create mode 100644 js/src/auto-instrumentations/import-in-the-middle/package.json create mode 100644 js/src/auto-instrumentations/require-in-the-middle/package.json create mode 100644 js/src/auto.ts create mode 100644 js/src/browser/isomorph.ts create mode 100644 js/src/browser/platform.ts create mode 100644 js/src/edge-light/isomorph.ts create mode 100644 js/src/edge-light/platform.ts create mode 100644 js/src/instrumentation/providers/cloudflare-think-context.test.ts create mode 100644 js/src/instrumentation/registrations/aisdk.ts create mode 100644 js/src/instrumentation/registrations/anthropic.ts create mode 100644 js/src/instrumentation/registrations/awsBedrockRuntime.ts create mode 100644 js/src/instrumentation/registrations/claudeAgentSDK.ts create mode 100644 js/src/instrumentation/registrations/cloudflareAIChat.ts create mode 100644 js/src/instrumentation/registrations/cloudflareAgents.ts create mode 100644 js/src/instrumentation/registrations/cloudflareThink.ts create mode 100644 js/src/instrumentation/registrations/cohere.ts create mode 100644 js/src/instrumentation/registrations/cursorSDK.ts create mode 100644 js/src/instrumentation/registrations/elevenlabs.ts create mode 100644 js/src/instrumentation/registrations/genkit.ts create mode 100644 js/src/instrumentation/registrations/gitHubCopilot.ts create mode 100644 js/src/instrumentation/registrations/googleADK.ts create mode 100644 js/src/instrumentation/registrations/googleGenAI.ts create mode 100644 js/src/instrumentation/registrations/googleGenerativeAI.ts create mode 100644 js/src/instrumentation/registrations/groq.ts create mode 100644 js/src/instrumentation/registrations/huggingface.ts create mode 100644 js/src/instrumentation/registrations/langchain.ts create mode 100644 js/src/instrumentation/registrations/langgraphSDK.ts create mode 100644 js/src/instrumentation/registrations/langsmith.ts create mode 100644 js/src/instrumentation/registrations/mastra.ts create mode 100644 js/src/instrumentation/registrations/mistral.ts create mode 100644 js/src/instrumentation/registrations/ollama.ts create mode 100644 js/src/instrumentation/registrations/openAIAgents.ts create mode 100644 js/src/instrumentation/registrations/openai.ts create mode 100644 js/src/instrumentation/registrations/openaiCodexSDK.ts create mode 100644 js/src/instrumentation/registrations/openrouter.ts create mode 100644 js/src/instrumentation/registrations/openrouterAgent.ts create mode 100644 js/src/instrumentation/registrations/piCodingAgent.ts create mode 100644 js/src/instrumentation/registrations/strandsAgentSDK.ts create mode 100644 js/src/instrumentation/registrations/typesafe.ts create mode 100644 js/src/instrumentation/registrations/voyageai.ts create mode 100644 js/src/isomorph-base.ts create mode 100644 js/src/logger/core.ts create mode 100644 js/src/logger/datasets.ts create mode 100644 js/src/logger/experiments.ts create mode 100644 js/src/logger/lifecycle.test.ts create mode 100644 js/src/logger/objects.ts create mode 100644 js/src/logger/prompts.ts create mode 100644 js/src/logger/testing.ts create mode 100644 js/src/node/isomorph.ts create mode 100644 js/src/node/platform.ts create mode 100644 js/src/workerd/isomorph.ts create mode 100644 js/src/workerd/platform.ts create mode 100644 js/src/wrappers/vitest/vite.config.ts delete mode 100644 js/src/wrappers/vitest/vitest.config.js delete mode 100644 js/tsup.config.ts create mode 100644 js/vite.config.ts delete mode 100644 js/vitest.config.js delete mode 100644 turbo.json create mode 100644 vite.config.ts diff --git a/.changeset/breezy-lines-draw.md b/.changeset/breezy-lines-draw.md new file mode 100644 index 000000000..c0bab996b --- /dev/null +++ b/.changeset/breezy-lines-draw.md @@ -0,0 +1,10 @@ +--- +"@braintrust/browser": major +"@braintrust/deepseek-harness": major +"@braintrust/otel": major +"@braintrust/templates-nunjucks-js": major +"@braintrust/temporal": major +"braintrust": major +--- + +build(sdk)!: Migrate to Vite+ and tree-shakeable ESM diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 262697300..7261c6abe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,12 +37,12 @@ updates: - "eslint" - "@typescript-eslint/*" - "prettier" - - "turbo" - "typescript" - - "tsup" - "tsx" - "vitest" - "vite" + - "vite-plus" + - "@voidzero-dev/*" - "vite-tsconfig-paths" - "rollup" - "webpack" diff --git a/.github/scripts/npm-download-stats.ts b/.github/scripts/npm-download-stats.ts index 02fae240e..e95e8d75b 100644 --- a/.github/scripts/npm-download-stats.ts +++ b/.github/scripts/npm-download-stats.ts @@ -212,7 +212,7 @@ async function main() { method: "POST", headers: { "Content-Type": "application/json", - "DD-API-KEY": ddApiKey, + "DD-API-KEY": ddApiKey!, }, body: JSON.stringify(payload), }); diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index a8d2fa3da..07ae07349 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -40,8 +40,8 @@ jobs: uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Lint - run: pnpm run lint + - name: Static checks + run: pnpm run check check-typings: runs-on: ubuntu-latest @@ -88,9 +88,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-2025-vs2026] - node-version: [20, 22, 24, 26] - env: - ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: ${{ matrix.node-version == 20 && 'true' || '' }} + node-version: [22.18.0, 24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -226,9 +224,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20] - env: - ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: "true" + node-version: [22.18.0] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -269,8 +265,11 @@ jobs: mkdir -p "$BASELINE_DIR/js/util" cp -R js/dist "$BASELINE_DIR/js/" - cp -R js/util/dist "$BASELINE_DIR/js/util/" - echo "Copied js/dist and js/util/dist to baseline" + cp js/package.json "$BASELINE_DIR/js/" + if [ -d js/util/dist ]; then + cp -R js/util/dist "$BASELINE_DIR/js/util/" + fi + echo "Copied package metadata and build outputs to baseline" git checkout "$CURRENT_REF" @@ -394,9 +393,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20, 22, 24, 26] - env: - ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: ${{ matrix.node-version == 20 && 'true' || '' }} + node-version: [22.18.0, 24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -422,9 +419,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20, 22, 24, 26] - env: - ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: ${{ matrix.node-version == 20 && 'true' || '' }} + node-version: [22.18.0, 24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -450,10 +445,8 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20, 22, 24, 26] + node-version: [22.18.0, 24, 26] test-dir: [otel-v1, otel-v2] - env: - ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: ${{ matrix.node-version == 20 && 'true' || '' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Docker Buildx diff --git a/.gitignore b/.gitignore index bf65941bd..bfe9f335e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ tsconfig.tsbuildinfo # Auto-generated by @sentry/dotagents — do not commit these files. agents.lock .agents/.gitignore + +# Local bundle reports and before/after build artifacts. +.context/ +.vite/ diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 8b8367a3c..000000000 --- a/.prettierignore +++ /dev/null @@ -1,17 +0,0 @@ -# Patch files use specific whitespace formatting required by unified diff format -patches/**/*.patch - -# E2E test snapshots -e2e/**/__snapshots__/**/* - -# Lock files -pnpm-lock.yaml -deno.lock - -# Template files with special formatting -js/scripts/openapi_zod_client_output_template.hbs - -# Generated dotagents skill content -.agents/skills/dotagents/ - -.context diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index f3bcd4c90..000000000 --- a/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "singleQuote": false, - "trailingComma": "all" -} diff --git a/AGENTS.md b/AGENTS.md index 6553edbca..1d69a37b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,8 @@ Span names should generally remain stable across calls and versions. Do not incl Uses Vitest. Prefer running the **narrowest relevant test** rather than the full suite. +Generally, do not add tests for build or linting code or their configuration. Validate tooling changes by running the relevant build or lint commands instead. + **From `js/` directory:** ```bash diff --git a/dev-packages/seinfeld/package.json b/dev-packages/seinfeld/package.json index 4ec7b366f..0bcdb6b9d 100644 --- a/dev-packages/seinfeld/package.json +++ b/dev-packages/seinfeld/package.json @@ -5,25 +5,24 @@ "type": "module", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22.18.0" }, - "main": "./dist/index.cjs", + "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" + "default": "./dist/index.js" } }, "files": [ "dist" ], "scripts": { - "build": "tsup", + "build": "vp run compile", "check:typings": "tsc --noEmit", - "dev": "tsup --watch", + "dev": "vp pack --watch", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -31,7 +30,8 @@ }, "devDependencies": { "@types/node": "^22.10.0", - "tsup": "^8.5.1", - "typescript": "^5.7.2" - } + "typescript": "^5.7.2", + "vite-plus": "catalog:" + }, + "sideEffects": false } diff --git a/dev-packages/seinfeld/src/format/v1.ts b/dev-packages/seinfeld/src/format/v1.ts index 558ddcfb5..a736337e3 100644 --- a/dev-packages/seinfeld/src/format/v1.ts +++ b/dev-packages/seinfeld/src/format/v1.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import type { CassetteFile } from "../cassette"; const bodyPayloadSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("empty") }), @@ -53,7 +54,7 @@ const cassetteMetaSchema = z }) .passthrough(); -export const cassetteSchema = z.object({ +export const cassetteSchema: z.ZodType = z.object({ version: z.any(), // TODO(luca): Remove after cassettes no longer have this field meta: cassetteMetaSchema.optional(), entries: z.array(cassetteEntrySchema), diff --git a/dev-packages/seinfeld/src/internal/well-known-headers.ts b/dev-packages/seinfeld/src/internal/well-known-headers.ts index 425e426a2..373185a40 100644 --- a/dev-packages/seinfeld/src/internal/well-known-headers.ts +++ b/dev-packages/seinfeld/src/internal/well-known-headers.ts @@ -3,7 +3,7 @@ * Keeping them in one place prevents the two subsystems from drifting. */ -export const TRANSPORT_HEADERS = [ +export const TRANSPORT_HEADERS: string[] = [ "content-encoding", "transfer-encoding", "connection", @@ -11,7 +11,7 @@ export const TRANSPORT_HEADERS = [ ]; /** Auth/session headers — stripped during matching and masked during redaction. */ -export const AUTH_HEADERS = [ +export const AUTH_HEADERS: string[] = [ "authorization", "api-key", "x-api-key", @@ -22,10 +22,10 @@ export const AUTH_HEADERS = [ "proxy-authorization", ]; -export const RATE_LIMIT_HEADERS = [ +export const RATE_LIMIT_HEADERS: Array = [ /^x-ratelimit-/i, /^x-rate-limit-/i, "retry-after", -] as Array; +]; -export const FINGERPRINT_HEADERS = ["user-agent"]; +export const FINGERPRINT_HEADERS: string[] = ["user-agent"]; diff --git a/dev-packages/seinfeld/tsconfig.json b/dev-packages/seinfeld/tsconfig.json index 6cb2a6d85..7c344b276 100644 --- a/dev-packages/seinfeld/tsconfig.json +++ b/dev-packages/seinfeld/tsconfig.json @@ -21,6 +21,6 @@ "rootDir": "./", "types": ["node"] }, - "include": ["src/**/*", "tsup.config.ts", "eslint.config.js"], + "include": ["src/**/*"], "exclude": ["node_modules", "dist", "examples"] } diff --git a/dev-packages/seinfeld/tsup.config.ts b/dev-packages/seinfeld/tsup.config.ts deleted file mode 100644 index 41c6c169c..000000000 --- a/dev-packages/seinfeld/tsup.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: { - index: "src/index.ts", - }, - format: ["esm", "cjs"], - dts: true, - clean: true, - sourcemap: true, - target: "node18", - splitting: false, - treeshake: true, -}); diff --git a/dev-packages/seinfeld/vite.config.ts b/dev-packages/seinfeld/vite.config.ts new file mode 100644 index 000000000..0a535fb72 --- /dev/null +++ b/dev-packages/seinfeld/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + run: { + tasks: { + compile: { + command: "vp pack", + dependsOn: [ + { task: "compile", from: ["dependencies", "devDependencies"] }, + ], + cache: { + input: [{ auto: true }, "!dist/**"], + untrackedEnv: ["SHELL", "BASH_ENV"], + output: ["dist/**"], + }, + }, + }, + }, + pack: { + dts: { generator: "oxc" }, + fixedExtension: false, + }, +}); diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 000000000..7a6c09645 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1 @@ +.vitest/ diff --git a/e2e/README.md b/e2e/README.md index 2f4b9c94d..99ea38169 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -23,7 +23,7 @@ e2e/ | |- scenario.test.ts # Colocated Vitest suite | |- package.json # Optional slim scenario-local deps | `- __snapshots__/ # Colocated snapshots -`- vitest.config.mts +`- vite.config.ts ``` Any extra files needed only by one scenario stay in that scenario folder. Anything reused by multiple scenarios belongs in `e2e/helpers/`. @@ -88,7 +88,7 @@ Some wrappers execute inside a nested test runner rather than a single SDK call. - Keep the outer e2e suite in `scenario.test.ts` and the spawned runner entrypoint in `scenario.ts`. - Put nested runner source in files like `runner.case.ts` or `runner.case.mjs`. -- Do not name nested runner files `*.test.ts`, because the outer `e2e/vitest.config.mts` includes `scenarios/**/*.test.ts` and will try to execute them directly. +- Do not name nested runner files `*.test.ts`, because the outer `e2e/vite.config.ts` includes `scenarios/**/*.test.ts` and will try to execute them directly. - Tag every traced test/eval with `metadata.testRunId` so the outer assertions can isolate rows across multiple trace roots with `payloadRowsForTestRunId(...)`. - If a nested runner needs its own test discovery rules, keep that config local to the scenario folder so the shared e2e config stays unchanged. diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index f12752b14..01d161916 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -800,6 +800,14 @@ { "variantKey": "vitest-v4-latest", "label": "v4 latest" + }, + { + "variantKey": "vitest-v5", + "label": "v5 pinned" + }, + { + "variantKey": "vitest-v5-latest", + "label": "v5 latest" } ], "evals": [ diff --git a/e2e/helpers/attachment-capture-assertions.ts b/e2e/helpers/attachment-capture-assertions.ts index 122a944c6..ad73b8bce 100644 --- a/e2e/helpers/attachment-capture-assertions.ts +++ b/e2e/helpers/attachment-capture-assertions.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { withScenarioHarness } from "./scenario-harness"; import { findLatestSpan } from "./trace-selectors"; import { captureCases } from "./attachment-capture-scenario.mjs"; diff --git a/e2e/helpers/file-snapshot.ts b/e2e/helpers/file-snapshot.ts index ce778ad80..a170fc8f7 100644 --- a/e2e/helpers/file-snapshot.ts +++ b/e2e/helpers/file-snapshot.ts @@ -1,6 +1,6 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { expect, type ExpectStatic } from "vitest"; +import { expect, type ExpectStatic } from "vite-plus/test"; import { normalizeForSnapshot, type Json } from "./normalize"; function sortJsonKeys(value: Json): Json { diff --git a/e2e/helpers/mock-braintrust-server.test.ts b/e2e/helpers/mock-braintrust-server.test.ts index 0a5d0bc91..4199d8fc6 100644 --- a/e2e/helpers/mock-braintrust-server.test.ts +++ b/e2e/helpers/mock-braintrust-server.test.ts @@ -1,7 +1,7 @@ import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import { setTimeout } from "node:timers/promises"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { startMockBraintrustServer } from "./mock-braintrust-server"; describe("production forwarding", () => { diff --git a/e2e/helpers/normalize.test.ts b/e2e/helpers/normalize.test.ts index b5a43532a..fe55ed4fc 100644 --- a/e2e/helpers/normalize.test.ts +++ b/e2e/helpers/normalize.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "vite-plus/test"; import { normalizeForSnapshot } from "./normalize"; describe("normalizeForSnapshot", () => { diff --git a/e2e/helpers/normalize.ts b/e2e/helpers/normalize.ts index c0c79b904..7d9281518 100644 --- a/e2e/helpers/normalize.ts +++ b/e2e/helpers/normalize.ts @@ -122,12 +122,12 @@ const CLAUDE_AGENT_OUTPUT_FILE_REGEX = const PROVIDER_HELPER_CALLER_REGEX = /^\/e2e\/helpers\/.+-scenario\.mjs$/; const ANTHROPIC_MESSAGE_STREAM_PATH_REGEX = /([/\\]node_modules[/\\]\.pnpm[/\\]@anthropic-ai\+sdk@[^/\\\s)]+[/\\]node_modules[/\\]@anthropic-ai[/\\]sdk[/\\])(?:src[/\\]lib[/\\]MessageStream\.ts|lib[/\\]MessageStream\.js)/g; -// tsup's `splitting: true` for our own `dist/` emits content-hashed chunk +// SDK builds emit content-hashed chunk // files (e.g. `/js/dist/chunk-7DWPOXBX.mjs`) whose names change any // time the bundle graph changes. Normalize them to a stable placeholder so // stack traces in error snapshots don't churn on unrelated bundle splits. const SDK_CHUNK_PATH_REGEX = - /(\/js\/dist\/)chunk-[A-Z0-9]+(\.(?:c?js|cjs|mjs))/g; + /(\/js\/dist\/(?:node\/|browser\/|workerd\/|edge-light\/)?)(?:chunk-[A-Z0-9]+|[\w-]+-[\w-]{8})(\.(?:c?js|cjs|mjs))/g; const ANTHROPIC_PNPM_VERSION_REGEX = /([/\\]\.pnpm[/\\]@anthropic-ai\+sdk@)[^/\\\s)]+/g; const LOCALHOST_HTTP_URL_SUBSTRING_REGEX = diff --git a/e2e/helpers/pr-e2e-links.test.ts b/e2e/helpers/pr-e2e-links.test.ts index 9f787de43..ccda1c25d 100644 --- a/e2e/helpers/pr-e2e-links.test.ts +++ b/e2e/helpers/pr-e2e-links.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; -import { expect, it } from "vitest"; +import { expect, it } from "vite-plus/test"; it("links published runs while preserving legacy records and excluding local-only runs", async () => { const dir = await mkdtemp(path.join(tmpdir(), "braintrust-e2e-links-")); @@ -20,6 +20,16 @@ it("links published runs while preserving legacy records and excluding local-onl label: "Scenario", metadataScenario: "scenario", }, + { + scenarioDirName: "without-metadata", + label: "Without metadata", + metadataScenario: "without-metadata", + }, + { + scenarioDirName: "experiment", + label: "Experiment", + metadataScenario: "experiment", + }, ]), ); await writeFile( @@ -28,6 +38,20 @@ it("links published runs while preserving legacy records and excluding local-onl { testRunId: "e2e-published", forwardToProduction: true }, { testRunId: "e2e-local-only", forwardToProduction: false }, { testRunId: "e2e-legacy" }, + { + scenarioDirName: "without-metadata", + testRunId: "e2e-no-metadata", + forwardToProduction: true, + logRootSpanIds: ["root-without-metadata"], + experimentNames: [], + }, + { + scenarioDirName: "experiment", + testRunId: "e2e-experiment", + forwardToProduction: true, + logRootSpanIds: [], + experimentNames: ["suite with spaces"], + }, ] .map((record) => JSON.stringify({ scenarioDirName: "scenario", ...record }), @@ -55,6 +79,21 @@ it("links published runs while preserving legacy records and excluding local-onl expect(stdout).toContain("e2e-published"); expect(stdout).toContain("e2e-legacy"); expect(stdout).not.toContain("e2e-local-only"); + const logRow = stdout + .split("\n") + .find((line) => line.startsWith("| Without metadata |"))!; + const logUrl = new URL(logRow.match(/\]\(([^)]+)\)/)![1]); + const search = JSON.parse(logUrl.searchParams.get("search")!); + expect( + search.filter.map((filter: { text: string }) => + decodeURIComponent(filter.text), + ), + ).toEqual(['root_span_id IN ("root-without-metadata")']); + const evalRow = stdout + .split("\n") + .find((line) => line.startsWith("| Experiment |"))!; + expect(evalRow).toContain("/experiments/suite%20with%20spaces"); + expect(evalRow).not.toContain("/logs?"); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/e2e/helpers/scenario-harness.ts b/e2e/helpers/scenario-harness.ts index 2a5d0fe67..1ca4981de 100644 --- a/e2e/helpers/scenario-harness.ts +++ b/e2e/helpers/scenario-harness.ts @@ -81,6 +81,8 @@ export interface ScenarioRunContext { interface ScenarioRunContextRecord { forwardToProduction?: boolean; + logRootSpanIds?: string[]; + experimentNames?: string[]; entry: string; runner: ScenarioRunner; scenarioDirName: string; @@ -843,7 +845,30 @@ export async function withScenarioHarness( ): Promise => { const result = await run(); await recordScenarioRunContext({ - forwardToProduction: optionsForHarness.forwardToProduction, + forwardToProduction: Boolean(prodForwarding), + logRootSpanIds: [ + ...new Set( + server.events.flatMap((event) => + event.projectId && !event.experimentId && event.span.rootId + ? [event.span.rootId] + : [], + ), + ), + ], + experimentNames: [ + ...new Set( + server.requests.flatMap((request) => { + const body = request.jsonBody; + return request.path === "/api/experiment/register" && + body && + typeof body === "object" && + !Array.isArray(body) && + typeof body.experiment_name === "string" + ? [body.experiment_name] + : []; + }), + ), + ], entry: options.entry ?? defaultEntry, runner, scenarioDirName: path.basename( diff --git a/e2e/helpers/span-tree.test.ts b/e2e/helpers/span-tree.test.ts index c4ec23ead..d683e2fcf 100644 --- a/e2e/helpers/span-tree.test.ts +++ b/e2e/helpers/span-tree.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "./mock-braintrust-server"; import { formatSpanTreeJsonSnapshot } from "./span-tree"; diff --git a/e2e/helpers/span-tree.ts b/e2e/helpers/span-tree.ts index c99d24d18..bcb68b3c7 100644 --- a/e2e/helpers/span-tree.ts +++ b/e2e/helpers/span-tree.ts @@ -1,4 +1,4 @@ -import type { ExpectStatic } from "vitest"; +import type { ExpectStatic } from "vite-plus/test"; import { matchFileSnapshot } from "./file-snapshot"; import type { CapturedLogEvent } from "./mock-braintrust-server"; import { diff --git a/e2e/package.json b/e2e/package.json index b6d5fe190..0612dfd3c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -19,7 +19,8 @@ "braintrust": "workspace:^", "tsx": "^4.21.0", "typescript": "5.4.4", - "vitest": "4.1.11", - "zod": "4.3.6" + "vitest": "catalog:", + "zod": "4.3.6", + "vite-plus": "catalog:" } } diff --git a/e2e/scenarios/ai-sdk-harness-instrumentation/scenario.test.ts b/e2e/scenarios/ai-sdk-harness-instrumentation/scenario.test.ts index 87054af1f..84c7ceaf1 100644 --- a/e2e/scenarios/ai-sdk-harness-instrumentation/scenario.test.ts +++ b/e2e/scenarios/ai-sdk-harness-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "vite-plus/test"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; @@ -49,7 +49,7 @@ const scenarios = await Promise.all( })), ); -describe.sequential("HarnessAgent instrumentation variants", () => { +describe("HarnessAgent instrumentation variants", { concurrent: false }, () => { test( "preserves a turn across a real subprocess handoff", { timeout: TIMEOUT_MS }, @@ -154,205 +154,211 @@ describe.sequential("HarnessAgent instrumentation variants", () => { ); for (const scenario of scenarios) { - describe.sequential(`@ai-sdk/harness ${scenario.version}`, () => { - for (const mode of ["wrapped", "auto-hook"] as const) { - test(mode, { timeout: TIMEOUT_MS }, async () => { - await withScenarioHarness(async (harness) => { - const runContext = { - variantKey: scenario.variantKey, - originalScenarioDir, - }; - const env = { - AI_SDK_HARNESS_CODEX_PACKAGE_NAME: scenario.codexDependencyName, - AI_SDK_HARNESS_PACKAGE_NAME: scenario.dependencyName, - }; - if (mode === "wrapped") { - await harness.runScenarioDir({ - entry: "scenario.ts", - env, - runContext, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - } else { - await harness.runNodeScenarioDir({ - entry: "scenario.mjs", - env, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - } + describe( + `@ai-sdk/harness ${scenario.version}`, + { concurrent: false }, + () => { + for (const mode of ["wrapped", "auto-hook"] as const) { + test(mode, { timeout: TIMEOUT_MS }, async () => { + await withScenarioHarness(async (harness) => { + const runContext = { + variantKey: scenario.variantKey, + originalScenarioDir, + }; + const env = { + AI_SDK_HARNESS_CODEX_PACKAGE_NAME: scenario.codexDependencyName, + AI_SDK_HARNESS_PACKAGE_NAME: scenario.dependencyName, + }; + if (mode === "wrapped") { + await harness.runScenarioDir({ + entry: "scenario.ts", + env, + runContext, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + } else { + await harness.runNodeScenarioDir({ + entry: "scenario.mjs", + env, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + } - const events = harness.events(); - const turns = turnNames.flatMap((name) => - findAllSpans(events, name), - ); - const turnsByName = Object.fromEntries( - turns.map((turn) => [turn.span.name, turn]), - ); - expect(turns).toHaveLength(2); - expect(new Set(turns.map((turn) => turn.span.rootId)).size).toBe(2); + const events = harness.events(); + const turns = turnNames.flatMap((name) => + findAllSpans(events, name), + ); + const turnsByName = Object.fromEntries( + turns.map((turn) => [turn.span.name, turn]), + ); + expect(turns).toHaveLength(2); + expect(new Set(turns.map((turn) => turn.span.rootId)).size).toBe( + 2, + ); - for (const turn of turns) { - expect(turn.span.type).toBe("task"); - expect(turn.metadata).toMatchObject({ - harnessId: "codex", - permissionMode: "allow-all", - scenario: "ai-sdk-harness-instrumentation", - sessionId: "shared-harness-session", - testRunId: harness.testRunId, + for (const turn of turns) { + expect(turn.span.type).toBe("task"); + expect(turn.metadata).toMatchObject({ + harnessId: "codex", + permissionMode: "allow-all", + scenario: "ai-sdk-harness-instrumentation", + sessionId: "shared-harness-session", + testRunId: harness.testRunId, + }); + expect(turn.input).not.toHaveProperty("session"); + } + expect( + turnsByName["HarnessAgent.generate"]?.span.parentIds, + ).toEqual([]); + expect( + turnsByName["HarnessAgent.stream"]?.span.parentIds, + ).toEqual([]); + expect( + findAllSpans(events, "HarnessAgent.continueGenerate"), + ).toEqual([]); + expect( + findAllSpans(events, "HarnessAgent.continueStream"), + ).toEqual([]); + expect(turnsByName["HarnessAgent.generate"]?.input).toEqual({ + prompt: + 'Run the built-in bash command "touch /workspace/generate-started; sleep 5; printf GENERATE_OK" exactly once. After it finishes, reply exactly GENERATE_OK.', }); - expect(turn.input).not.toHaveProperty("session"); - } - expect( - turnsByName["HarnessAgent.generate"]?.span.parentIds, - ).toEqual([]); - expect(turnsByName["HarnessAgent.stream"]?.span.parentIds).toEqual( - [], - ); - expect( - findAllSpans(events, "HarnessAgent.continueGenerate"), - ).toEqual([]); - expect(findAllSpans(events, "HarnessAgent.continueStream")).toEqual( - [], - ); - expect(turnsByName["HarnessAgent.generate"]?.input).toEqual({ - prompt: - 'Run the built-in bash command "touch /workspace/generate-started; sleep 5; printf GENERATE_OK" exactly once. After it finishes, reply exactly GENERATE_OK.', - }); - expect(turnsByName["HarnessAgent.stream"]?.input).toEqual({ - messages: [ - { - role: "user", - content: - 'Run the built-in bash command "touch /workspace/stream-started; sleep 5; printf STREAM_OK" exactly once. After it finishes, reply exactly STREAM_OK.', - }, - ], - }); - for (const turn of turns) { - expect(turn.output).toBeDefined(); - expect(turn.metrics?.tokens).toEqual(expect.any(Number)); - expect(turn.metrics?.tokens).toBeGreaterThan(0); - } - - expect(findAllSpans(events, "HarnessAgent.createSession")).toEqual( - [], - ); - expect(findAllSpans(events, "doGenerate").length).toBeGreaterThan( - 0, - ); - const harnessSpans = findAllSpans(events, "harness"); - expect(harnessSpans).toHaveLength(4); - const bashSpans = findAllSpans(events, "bash"); - expect(bashSpans).toHaveLength(2); - for (const bashSpan of bashSpans) { - expect(bashSpan.span.type).toBe("tool"); - expect(bashSpan.span.parentIds).toHaveLength(1); - expect(bashSpan.metadata).toMatchObject({ - toolName: "bash", + expect(turnsByName["HarnessAgent.stream"]?.input).toEqual({ + messages: [ + { + role: "user", + content: + 'Run the built-in bash command "touch /workspace/stream-started; sleep 5; printf STREAM_OK" exactly once. After it finishes, reply exactly STREAM_OK.', + }, + ], }); - } + for (const turn of turns) { + expect(turn.output).toBeDefined(); + expect(turn.metrics?.tokens).toEqual(expect.any(Number)); + expect(turn.metrics?.tokens).toBeGreaterThan(0); + } - const generateBashSpan = bashSpans.find((span) => - String(span.input).includes("/workspace/generate-started"), - ); - const streamBashSpan = bashSpans.find((span) => - String(span.input).includes("/workspace/stream-started"), - ); - expect(generateBashSpan?.input).toBe( - '{"command":"/bin/bash -lc \'touch /workspace/generate-started; sleep 5; printf GENERATE_OK\'"}', - ); - expect(streamBashSpan?.input).toBe( - '{"command":"/bin/bash -lc \'touch /workspace/stream-started; sleep 5; printf STREAM_OK\'"}', - ); - expect(generateBashSpan?.span.rootId).toBe( - turnsByName["HarnessAgent.generate"]?.span.rootId, - ); - expect(generateBashSpan?.span.parentIds).toEqual([ - turnsByName["HarnessAgent.generate"]?.span.id, - ]); - expect(streamBashSpan?.span.rootId).toBe( - turnsByName["HarnessAgent.stream"]?.span.rootId, - ); - expect(streamBashSpan?.span.parentIds).toEqual([ - turnsByName["HarnessAgent.stream"]?.span.id, - ]); - for (const [toolSpan, initialName] of [ - [generateBashSpan, "HarnessAgent.generate"], - [streamBashSpan, "HarnessAgent.stream"], - ] as const) { expect( - numericMetric(turnsByName[initialName], "start"), - ).toBeLessThanOrEqual(numericMetric(toolSpan, "start")); - expect(numericMetric(toolSpan, "start")).toBeLessThanOrEqual( - numericMetric(turnsByName[initialName], "end"), + findAllSpans(events, "HarnessAgent.createSession"), + ).toEqual([]); + expect(findAllSpans(events, "doGenerate").length).toBeGreaterThan( + 0, ); - - const turn = turnsByName[initialName]; - const operations = harnessSpans - .filter((span) => span.span.rootId === turn?.span.rootId) - .sort( - (left, right) => - numericMetric(left, "start") - - numericMetric(right, "start"), - ); - expect(operations).toHaveLength(2); - for (const operation of operations) { - expect(operation.span.parentIds).toEqual([turn?.span.id]); + const harnessSpans = findAllSpans(events, "harness"); + expect(harnessSpans).toHaveLength(4); + const bashSpans = findAllSpans(events, "bash"); + expect(bashSpans).toHaveLength(2); + for (const bashSpan of bashSpans) { + expect(bashSpan.span.type).toBe("tool"); + expect(bashSpan.span.parentIds).toHaveLength(1); + expect(bashSpan.metadata).toMatchObject({ + toolName: "bash", + }); } - expect(numericMetric(operations[0], "start")).toBeLessThanOrEqual( - numericMetric(operations[1], "start"), + + const generateBashSpan = bashSpans.find((span) => + String(span.input).includes("/workspace/generate-started"), + ); + const streamBashSpan = bashSpans.find((span) => + String(span.input).includes("/workspace/stream-started"), ); - const modelSpans = findAllSpans(events, "doGenerate").filter( - (span) => - operations.some((operation) => - span.span.parentIds.includes(operation.span.id), - ), + expect(generateBashSpan?.input).toBe( + '{"command":"/bin/bash -lc \'touch /workspace/generate-started; sleep 5; printf GENERATE_OK\'"}', ); - expect(modelSpans).toHaveLength(2); - for (const operation of operations) { + expect(streamBashSpan?.input).toBe( + '{"command":"/bin/bash -lc \'touch /workspace/stream-started; sleep 5; printf STREAM_OK\'"}', + ); + expect(generateBashSpan?.span.rootId).toBe( + turnsByName["HarnessAgent.generate"]?.span.rootId, + ); + expect(generateBashSpan?.span.parentIds).toEqual([ + turnsByName["HarnessAgent.generate"]?.span.id, + ]); + expect(streamBashSpan?.span.rootId).toBe( + turnsByName["HarnessAgent.stream"]?.span.rootId, + ); + expect(streamBashSpan?.span.parentIds).toEqual([ + turnsByName["HarnessAgent.stream"]?.span.id, + ]); + for (const [toolSpan, initialName] of [ + [generateBashSpan, "HarnessAgent.generate"], + [streamBashSpan, "HarnessAgent.stream"], + ] as const) { expect( - modelSpans.filter((span) => - span.span.parentIds.includes(operation.span.id), - ), - ).toHaveLength(1); + numericMetric(turnsByName[initialName], "start"), + ).toBeLessThanOrEqual(numericMetric(toolSpan, "start")); + expect(numericMetric(toolSpan, "start")).toBeLessThanOrEqual( + numericMetric(turnsByName[initialName], "end"), + ); + + const turn = turnsByName[initialName]; + const operations = harnessSpans + .filter((span) => span.span.rootId === turn?.span.rootId) + .sort( + (left, right) => + numericMetric(left, "start") - + numericMetric(right, "start"), + ); + expect(operations).toHaveLength(2); + for (const operation of operations) { + expect(operation.span.parentIds).toEqual([turn?.span.id]); + } + expect( + numericMetric(operations[0], "start"), + ).toBeLessThanOrEqual(numericMetric(operations[1], "start")); + const modelSpans = findAllSpans(events, "doGenerate").filter( + (span) => + operations.some((operation) => + span.span.parentIds.includes(operation.span.id), + ), + ); + expect(modelSpans).toHaveLength(2); + for (const operation of operations) { + expect( + modelSpans.filter((span) => + span.span.parentIds.includes(operation.span.id), + ), + ).toHaveLength(1); + } } - } - // Harness releases report provider-executed tool timing at - // different lifecycle points. The focused assertions above cover - // its ownership and root lifetime without inventing a boundary. - const snapshotEvents = events.filter( - (event) => event.span.name !== "bash", - ); - await matchSpanTreeSnapshot( - snapshotEvents, - resolveFileSnapshotPath( - import.meta.url, - `${scenario.variantKey}-${mode}.span-tree.json`, - ), - { - normalize: { - // These fields can be split across either side of a - // suspended Codex turn. The trace hierarchy and model - // ordering remain stable and are snapshot-tested. - omittedKeys: [ - "callId", - "content", - "responseMessages", - "steps", - "text", - "toolCalls", - "toolResults", - ], + // Harness releases report provider-executed tool timing at + // different lifecycle points. The focused assertions above cover + // its ownership and root lifetime without inventing a boundary. + const snapshotEvents = events.filter( + (event) => event.span.name !== "bash", + ); + await matchSpanTreeSnapshot( + snapshotEvents, + resolveFileSnapshotPath( + import.meta.url, + `${scenario.variantKey}-${mode}.span-tree.json`, + ), + { + normalize: { + // These fields can be split across either side of a + // suspended Codex turn. The trace hierarchy and model + // ordering remain stable and are snapshot-tested. + omittedKeys: [ + "callId", + "content", + "responseMessages", + "steps", + "text", + "toolCalls", + "toolResults", + ], + }, }, - }, - ); + ); + }); }); - }); - } - }); + } + }, + ); } }); diff --git a/e2e/scenarios/ai-sdk-instrumentation/assertions.ts b/e2e/scenarios/ai-sdk-instrumentation/assertions.ts index 7aaa0f077..7043e7628 100644 --- a/e2e/scenarios/ai-sdk-instrumentation/assertions.ts +++ b/e2e/scenarios/ai-sdk-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { Json } from "../../helpers/normalize"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; diff --git a/e2e/scenarios/ai-sdk-instrumentation/scenario.concurrent-shared-model.test.ts b/e2e/scenarios/ai-sdk-instrumentation/scenario.concurrent-shared-model.test.ts index 25f494da9..012c08874 100644 --- a/e2e/scenarios/ai-sdk-instrumentation/scenario.concurrent-shared-model.test.ts +++ b/e2e/scenarios/ai-sdk-instrumentation/scenario.concurrent-shared-model.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { diff --git a/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts index 616ea11d3..fad2b1ef7 100644 --- a/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -32,7 +32,7 @@ function parseMajorVersion(version: string): number { const describeVariants = process.env.BRAINTRUST_E2E_CASSETTE_MODE === "record" || process.env.BRAINTRUST_E2E_CASSETTE_MODE === "record-missing" - ? describe.sequential + ? describe : describe.concurrent; describeVariants("variants", () => { @@ -47,7 +47,7 @@ describeVariants("variants", () => { const supportsOutputObjectScenario = scenario.supportsOutputObjectScenario ?? supportsRichInputScenarios; - describe.sequential(`ai sdk ${scenario.version}`, () => { + describe(`ai sdk ${scenario.version}`, { concurrent: false }, () => { defineAISDKInstrumentationAssertions({ agentSpanName: scenario.agentSpanName, name: scenario.wrapperTestName ?? "wrapped instrumentation", diff --git a/e2e/scenarios/ai-sdk-otel-export/scenario.test.ts b/e2e/scenarios/ai-sdk-otel-export/scenario.test.ts index 0454cb62e..005290caa 100644 --- a/e2e/scenarios/ai-sdk-otel-export/scenario.test.ts +++ b/e2e/scenarios/ai-sdk-otel-export/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, matchFileSnapshot, diff --git a/e2e/scenarios/anthropic-bedrock-instrumentation/assertions.ts b/e2e/scenarios/anthropic-bedrock-instrumentation/assertions.ts index bba4e89eb..c89739151 100644 --- a/e2e/scenarios/anthropic-bedrock-instrumentation/assertions.ts +++ b/e2e/scenarios/anthropic-bedrock-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/anthropic-bedrock-instrumentation/scenario.test.ts b/e2e/scenarios/anthropic-bedrock-instrumentation/scenario.test.ts index b6aaa2652..13bb4d8a4 100644 --- a/e2e/scenarios/anthropic-bedrock-instrumentation/scenario.test.ts +++ b/e2e/scenarios/anthropic-bedrock-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -32,71 +32,75 @@ const anthropicBedrockScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of anthropicBedrockScenarios) { - describe.sequential(`anthropic bedrock sdk ${scenario.version}`, () => { - defineAnthropicBedrockInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: { - ANTHROPIC_BEDROCK_PACKAGE_NAME: scenario.dependencyName, - }, - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-wrapped`, - testFileUrl: import.meta.url, - timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, - }); + describe( + `anthropic bedrock sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineAnthropicBedrockInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { + ANTHROPIC_BEDROCK_PACKAGE_NAME: scenario.dependencyName, + }, + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, + }); - defineAnthropicBedrockInstrumentationAssertions({ - name: "auto-hook instrumentation ESM", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - env: { - ANTHROPIC_BEDROCK_PACKAGE_NAME: scenario.dependencyName, - }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-auto-esm`, - testFileUrl: import.meta.url, - timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, - }); + defineAnthropicBedrockInstrumentationAssertions({ + name: "auto-hook instrumentation ESM", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { + ANTHROPIC_BEDROCK_PACKAGE_NAME: scenario.dependencyName, + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-auto-esm`, + testFileUrl: import.meta.url, + timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, + }); - defineAnthropicBedrockInstrumentationAssertions({ - name: "auto-hook instrumentation CJS", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.cjs", - env: { - ANTHROPIC_BEDROCK_PACKAGE_NAME: scenario.dependencyName, - }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-auto-cjs`, - testFileUrl: import.meta.url, - timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, - }); - }); + defineAnthropicBedrockInstrumentationAssertions({ + name: "auto-hook instrumentation CJS", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.cjs", + env: { + ANTHROPIC_BEDROCK_PACKAGE_NAME: scenario.dependencyName, + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-auto-cjs`, + testFileUrl: import.meta.url, + timeoutMs: ANTHROPIC_BEDROCK_SCENARIO_TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/anthropic-instrumentation/assertions.ts b/e2e/scenarios/anthropic-instrumentation/assertions.ts index 812053b00..2b131024c 100644 --- a/e2e/scenarios/anthropic-instrumentation/assertions.ts +++ b/e2e/scenarios/anthropic-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { Json } from "../../helpers/normalize"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; diff --git a/e2e/scenarios/anthropic-instrumentation/scenario.test.ts b/e2e/scenarios/anthropic-instrumentation/scenario.test.ts index 32dc02950..4478ca6d5 100644 --- a/e2e/scenarios/anthropic-instrumentation/scenario.test.ts +++ b/e2e/scenarios/anthropic-instrumentation/scenario.test.ts @@ -1,5 +1,5 @@ import { defineAttachmentCaptureTests } from "../../helpers/attachment-capture-assertions"; -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -47,7 +47,7 @@ const anthropicScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of anthropicScenarios) { - describe.sequential(`anthropic sdk ${scenario.version}`, () => { + describe(`anthropic sdk ${scenario.version}`, { concurrent: false }, () => { defineAnthropicInstrumentationAssertions({ name: "wrapped instrumentation", runScenario: async ({ runScenarioDir }) => { diff --git a/e2e/scenarios/anthropic-vertex-instrumentation/assertions.ts b/e2e/scenarios/anthropic-vertex-instrumentation/assertions.ts index d42504c3c..2e10f7795 100644 --- a/e2e/scenarios/anthropic-vertex-instrumentation/assertions.ts +++ b/e2e/scenarios/anthropic-vertex-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { withScenarioHarness } from "../../helpers/scenario-harness"; diff --git a/e2e/scenarios/anthropic-vertex-instrumentation/scenario.test.ts b/e2e/scenarios/anthropic-vertex-instrumentation/scenario.test.ts index b759c390e..befb5e4de 100644 --- a/e2e/scenarios/anthropic-vertex-instrumentation/scenario.test.ts +++ b/e2e/scenarios/anthropic-vertex-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -32,74 +32,78 @@ const anthropicVertexScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of anthropicVertexScenarios) { - describe.sequential(`anthropic vertex sdk ${scenario.version}`, () => { - defineAnthropicVertexInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: { - ANTHROPIC_VERTEX_PACKAGE_NAME: scenario.dependencyName, - }, - runContext: { - cassette: false, - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-wrapped`, - testFileUrl: import.meta.url, - timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, - }); + describe( + `anthropic vertex sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineAnthropicVertexInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { + ANTHROPIC_VERTEX_PACKAGE_NAME: scenario.dependencyName, + }, + runContext: { + cassette: false, + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, + }); - defineAnthropicVertexInstrumentationAssertions({ - name: "auto-hook instrumentation ESM", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - env: { - ANTHROPIC_VERTEX_PACKAGE_NAME: scenario.dependencyName, - }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - cassette: false, - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-auto-esm`, - testFileUrl: import.meta.url, - timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, - }); + defineAnthropicVertexInstrumentationAssertions({ + name: "auto-hook instrumentation ESM", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { + ANTHROPIC_VERTEX_PACKAGE_NAME: scenario.dependencyName, + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + cassette: false, + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-auto-esm`, + testFileUrl: import.meta.url, + timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, + }); - defineAnthropicVertexInstrumentationAssertions({ - name: "auto-hook instrumentation CJS", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.cjs", - env: { - ANTHROPIC_VERTEX_PACKAGE_NAME: scenario.dependencyName, - }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - cassette: false, - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-auto-cjs`, - testFileUrl: import.meta.url, - timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, - }); - }); + defineAnthropicVertexInstrumentationAssertions({ + name: "auto-hook instrumentation CJS", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.cjs", + env: { + ANTHROPIC_VERTEX_PACKAGE_NAME: scenario.dependencyName, + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + cassette: false, + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-auto-cjs`, + testFileUrl: import.meta.url, + timeoutMs: ANTHROPIC_VERTEX_SCENARIO_TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/bedrock-runtime-instrumentation/assertions.ts b/e2e/scenarios/bedrock-runtime-instrumentation/assertions.ts index 406113a5b..f5a6ffc45 100644 --- a/e2e/scenarios/bedrock-runtime-instrumentation/assertions.ts +++ b/e2e/scenarios/bedrock-runtime-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/bedrock-runtime-instrumentation/scenario.test.ts b/e2e/scenarios/bedrock-runtime-instrumentation/scenario.test.ts index 6fc954e8b..62e7b3f99 100644 --- a/e2e/scenarios/bedrock-runtime-instrumentation/scenario.test.ts +++ b/e2e/scenarios/bedrock-runtime-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -32,45 +32,49 @@ const bedrockRuntimeScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of bedrockRuntimeScenarios) { - describe.sequential(`bedrock runtime sdk ${scenario.version}`, () => { - defineBedrockRuntimeInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: { BEDROCK_RUNTIME_PACKAGE_NAME: scenario.dependencyName }, - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: BEDROCK_RUNTIME_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-wrapped`, - testFileUrl: import.meta.url, - timeoutMs: BEDROCK_RUNTIME_SCENARIO_TIMEOUT_MS, - }); + describe( + `bedrock runtime sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineBedrockRuntimeInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { BEDROCK_RUNTIME_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: BEDROCK_RUNTIME_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: BEDROCK_RUNTIME_SCENARIO_TIMEOUT_MS, + }); - defineBedrockRuntimeInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - env: { BEDROCK_RUNTIME_PACKAGE_NAME: scenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: BEDROCK_RUNTIME_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-auto`, - testFileUrl: import.meta.url, - timeoutMs: BEDROCK_RUNTIME_SCENARIO_TIMEOUT_MS, - }); - }); + defineBedrockRuntimeInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { BEDROCK_RUNTIME_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: BEDROCK_RUNTIME_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-auto`, + testFileUrl: import.meta.url, + timeoutMs: BEDROCK_RUNTIME_SCENARIO_TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-auto-hook.span-tree.json b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-auto-hook.span-tree.json index 2eb4f2250..ee005765f 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-auto-hook.span-tree.json +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-auto-hook.span-tree.json @@ -969,7 +969,7 @@ { "name": "calculator-local-handler-divide", "children": [], - "error": "division by zero\n\nError: division by zero\n at traced.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.mjs:0:0)\n at withCurrent (/js/dist/index.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at runCatchFinally (/js/dist/index.mjs:0:0)\n at traced (/js/dist/index.mjs:0:0)\n at /e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0\n at runHandler (/js/dist/index.mjs:0:0)" + "error": "division by zero\n\nError: division by zero\n at traced.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/node/logger/core.js:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/node/logger/core.js:0:0)\n at withCurrent (/js/dist/node/logger/core.js:0:0)\n at /js/dist/node/logger/core.js:0:0\n at runCatchFinally (/js/dist/node/util.js:0:0)\n at traced (/js/dist/node/logger/core.js:0:0)\n at /e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0\n at runHandler (/js/dist/node/instrumentation/providers/claude-agent-sdk-local-tool-spans.js:0:0)" } ], "input": { diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-auto-hook.span-tree.txt b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-auto-hook.span-tree.txt index da47a60b1..378829e92 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-auto-hook.span-tree.txt +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-auto-hook.span-tree.txt @@ -880,7 +880,7 @@ span_tree: │ } │ error: "division by zero" │ └── calculator-local-handler-divide - │ error: "division by zero\n\nError: division by zero\n at traced.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.mjs:0:0)\n at withCurrent (/js/dist/index.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at runCatchFinally (/js/dist/index.mjs:0:0)\n at traced (/js/dist/index.mjs:0:0)\n at /e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0\n at runHandler (/js/dist/index.mjs:0:0)" + │ error: "division by zero\n\nError: division by zero\n at traced.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/node/logger/core.js:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/node/logger/core.js:0:0)\n at withCurrent (/js/dist/node/logger/core.js:0:0)\n at /js/dist/node/logger/core.js:0:0\n at runCatchFinally (/js/dist/node/util.js:0:0)\n at traced (/js/dist/node/logger/core.js:0:0)\n at /e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0\n at runHandler (/js/dist/node/instrumentation/providers/claude-agent-sdk-local-tool-spans.js:0:0)" └── anthropic.messages.create [llm] input: [ { diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-auto-hook.span-tree.json b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-auto-hook.span-tree.json index d6336477c..8e21617ea 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-auto-hook.span-tree.json +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-auto-hook.span-tree.json @@ -1022,7 +1022,7 @@ { "name": "calculator-local-handler-divide", "children": [], - "error": "division by zero\n\nError: division by zero\n at traced.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.mjs:0:0)\n at withCurrent (/js/dist/index.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at runCatchFinally (/js/dist/index.mjs:0:0)\n at traced (/js/dist/index.mjs:0:0)\n at /e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0\n at runHandler (/js/dist/index.mjs:0:0)" + "error": "division by zero\n\nError: division by zero\n at traced.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/node/logger/core.js:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/node/logger/core.js:0:0)\n at withCurrent (/js/dist/node/logger/core.js:0:0)\n at /js/dist/node/logger/core.js:0:0\n at runCatchFinally (/js/dist/node/util.js:0:0)\n at traced (/js/dist/node/logger/core.js:0:0)\n at /e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0\n at runHandler (/js/dist/node/instrumentation/providers/claude-agent-sdk-local-tool-spans.js:0:0)" } ], "input": { diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-auto-hook.span-tree.txt b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-auto-hook.span-tree.txt index 7472de4ad..c02996c65 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-auto-hook.span-tree.txt +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-auto-hook.span-tree.txt @@ -933,7 +933,7 @@ span_tree: │ } │ error: "division by zero" │ └── calculator-local-handler-divide - │ error: "division by zero\n\nError: division by zero\n at traced.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.mjs:0:0)\n at withCurrent (/js/dist/index.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at runCatchFinally (/js/dist/index.mjs:0:0)\n at traced (/js/dist/index.mjs:0:0)\n at /e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0\n at runHandler (/js/dist/index.mjs:0:0)" + │ error: "division by zero\n\nError: division by zero\n at traced.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/node/logger/core.js:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/node/logger/core.js:0:0)\n at withCurrent (/js/dist/node/logger/core.js:0:0)\n at /js/dist/node/logger/core.js:0:0\n at runCatchFinally (/js/dist/node/util.js:0:0)\n at traced (/js/dist/node/logger/core.js:0:0)\n at /e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0\n at runHandler (/js/dist/node/instrumentation/providers/claude-agent-sdk-local-tool-spans.js:0:0)" └── anthropic.messages.create [llm] input: [ { diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-wrapped.span-tree.json b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-wrapped.span-tree.json index b2ee74b84..b6299642f 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-wrapped.span-tree.json +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-wrapped.span-tree.json @@ -1022,7 +1022,7 @@ { "name": "calculator-local-handler-divide", "children": [], - "error": "division by zero\n\nError: division by zero\n at result.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/index.js:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.js:0:0)\n at withCurrent (/js/dist/index.js:0:0)\n at /js/dist/index.js:0:0\n at runCatchFinally (/js/dist/index.js:0:0)\n at traced (/js/dist/index.js:0:0)\n at (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at runHandler (/js/dist/index.js:0:0)" + "error": "division by zero\n\nError: division by zero\n at result.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at (/js/dist/node/logger/core.js:0:0)\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/node/logger/core.js:0:0)\n at withCurrent (/js/dist/node/logger/core.js:0:0)\n at (/js/dist/node/logger/core.js:0:0)\n at runCatchFinally (/js/dist/node/util.js:0:0)\n at traced (/js/dist/node/logger/core.js:0:0)\n at (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at runHandler (/js/dist/node/instrumentation/providers/claude-agent-sdk-local-tool-spans.js:0:0)" } ], "input": { diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-wrapped.span-tree.txt b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-wrapped.span-tree.txt index a0ba863a6..90a63cdd1 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-wrapped.span-tree.txt +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-latest-wrapped.span-tree.txt @@ -933,7 +933,7 @@ span_tree: │ } │ error: "division by zero" │ └── calculator-local-handler-divide - │ error: "division by zero\n\nError: division by zero\n at result.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/index.js:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.js:0:0)\n at withCurrent (/js/dist/index.js:0:0)\n at /js/dist/index.js:0:0\n at runCatchFinally (/js/dist/index.js:0:0)\n at traced (/js/dist/index.js:0:0)\n at (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at runHandler (/js/dist/index.js:0:0)" + │ error: "division by zero\n\nError: division by zero\n at result.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at (/js/dist/node/logger/core.js:0:0)\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/node/logger/core.js:0:0)\n at withCurrent (/js/dist/node/logger/core.js:0:0)\n at (/js/dist/node/logger/core.js:0:0)\n at runCatchFinally (/js/dist/node/util.js:0:0)\n at traced (/js/dist/node/logger/core.js:0:0)\n at (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at runHandler (/js/dist/node/instrumentation/providers/claude-agent-sdk-local-tool-spans.js:0:0)" └── anthropic.messages.create [llm] input: [ { diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-wrapped.span-tree.json b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-wrapped.span-tree.json index dfbed7125..10e14c123 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-wrapped.span-tree.json +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-wrapped.span-tree.json @@ -969,7 +969,7 @@ { "name": "calculator-local-handler-divide", "children": [], - "error": "division by zero\n\nError: division by zero\n at result.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/index.js:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.js:0:0)\n at withCurrent (/js/dist/index.js:0:0)\n at /js/dist/index.js:0:0\n at runCatchFinally (/js/dist/index.js:0:0)\n at traced (/js/dist/index.js:0:0)\n at (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at runHandler (/js/dist/index.js:0:0)" + "error": "division by zero\n\nError: division by zero\n at result.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at (/js/dist/node/logger/core.js:0:0)\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/node/logger/core.js:0:0)\n at withCurrent (/js/dist/node/logger/core.js:0:0)\n at (/js/dist/node/logger/core.js:0:0)\n at runCatchFinally (/js/dist/node/util.js:0:0)\n at traced (/js/dist/node/logger/core.js:0:0)\n at (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at runHandler (/js/dist/node/instrumentation/providers/claude-agent-sdk-local-tool-spans.js:0:0)" } ], "input": { diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-wrapped.span-tree.txt b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-wrapped.span-tree.txt index c62370089..28f28e70d 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-wrapped.span-tree.txt +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/__snapshots__/claude-agent-sdk-v0-wrapped.span-tree.txt @@ -880,7 +880,7 @@ span_tree: │ } │ error: "division by zero" │ └── calculator-local-handler-divide - │ error: "division by zero\n\nError: division by zero\n at result.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at /js/dist/index.js:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.js:0:0)\n at withCurrent (/js/dist/index.js:0:0)\n at /js/dist/index.js:0:0\n at runCatchFinally (/js/dist/index.js:0:0)\n at traced (/js/dist/index.js:0:0)\n at (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at runHandler (/js/dist/index.js:0:0)" + │ error: "division by zero\n\nError: division by zero\n at result.name (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at (/js/dist/node/logger/core.js:0:0)\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/node/logger/core.js:0:0)\n at withCurrent (/js/dist/node/logger/core.js:0:0)\n at (/js/dist/node/logger/core.js:0:0)\n at runCatchFinally (/js/dist/node/util.js:0:0)\n at traced (/js/dist/node/logger/core.js:0:0)\n at (/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.impl.mjs:0:0)\n at runHandler (/js/dist/node/instrumentation/providers/claude-agent-sdk-local-tool-spans.js:0:0)" └── anthropic.messages.create [llm] input: [ { diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/assertions.ts b/e2e/scenarios/claude-agent-sdk-instrumentation/assertions.ts index b0d7cd806..49fd5ba0d 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/assertions.ts +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { Json } from "../../helpers/normalize"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; diff --git a/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.test.ts index 7b93dcbef..a4f667a2f 100644 --- a/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/claude-agent-sdk-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -41,53 +41,61 @@ const claudeAgentSDKScenarios = await Promise.all( describe.concurrent("wrapped instrumentation", () => { for (const scenario of claudeAgentSDKScenarios) { - describe.sequential(`claude agent sdk ${scenario.version}`, () => { - defineClaudeAgentSDKInstrumentationAssertions({ - assertLocalToolHandlerParenting: true, - expectTaskLifecycleDetails: scenario.expectTaskLifecycleDetails, - name: "scenario", - runScenario: ({ runScenarioDir }) => - runScenarioDir({ - entry: scenario.wrapperEntry, - env: { CLAUDE_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName }, - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }), - snapshotName: `${scenario.snapshotName}-wrapped`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + describe( + `claude agent sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineClaudeAgentSDKInstrumentationAssertions({ + assertLocalToolHandlerParenting: true, + expectTaskLifecycleDetails: scenario.expectTaskLifecycleDetails, + name: "scenario", + runScenario: ({ runScenarioDir }) => + runScenarioDir({ + entry: scenario.wrapperEntry, + env: { CLAUDE_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }), + snapshotName: `${scenario.snapshotName}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); describe.concurrent("auto-hook instrumentation", () => { for (const scenario of claudeAgentSDKScenarios) { - describe.sequential(`claude agent sdk ${scenario.version}`, () => { - defineClaudeAgentSDKInstrumentationAssertions({ - assertLocalToolHandlerParenting: true, - expectTaskLifecycleDetails: scenario.expectTaskLifecycleDetails, - name: "scenario", - runScenario: ({ runNodeScenarioDir }) => - runNodeScenarioDir({ - entry: scenario.autoEntry, - env: { CLAUDE_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }), - snapshotName: `${scenario.snapshotName}-auto-hook`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + describe( + `claude agent sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineClaudeAgentSDKInstrumentationAssertions({ + assertLocalToolHandlerParenting: true, + expectTaskLifecycleDetails: scenario.expectTaskLifecycleDetails, + name: "scenario", + runScenario: ({ runNodeScenarioDir }) => + runNodeScenarioDir({ + entry: scenario.autoEntry, + env: { CLAUDE_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }), + snapshotName: `${scenario.snapshotName}-auto-hook`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/cloudflare-agents-instrumentation/assertions.ts b/e2e/scenarios/cloudflare-agents-instrumentation/assertions.ts index bba5b0b90..a4f6c5503 100644 --- a/e2e/scenarios/cloudflare-agents-instrumentation/assertions.ts +++ b/e2e/scenarios/cloudflare-agents-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/cloudflare-agents-instrumentation/scenario.test.ts b/e2e/scenarios/cloudflare-agents-instrumentation/scenario.test.ts index 09929d559..794068f05 100644 --- a/e2e/scenarios/cloudflare-agents-instrumentation/scenario.test.ts +++ b/e2e/scenarios/cloudflare-agents-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -34,30 +34,34 @@ const scenarios = await Promise.all( describe.concurrent("Cloudflare Agents versions", () => { for (const scenario of scenarios) { - describe.sequential(`${scenario.label} (${scenario.version})`, () => { - for (const mode of ["wrapped", "auto"] as const) { - defineCloudflareAgentsAssertions({ - name: `${mode} instrumentation`, - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: { - CLOUDFLARE_AGENTS_INSTRUMENTATION_MODE: mode, - CLOUDFLARE_AGENTS_PACKAGE_NAME: scenario.dependencyName, - }, - runContext: { - cassette: false, - variantKey: scenario.variantKey, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.variantKey}-${mode}`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - } - }); + describe( + `${scenario.label} (${scenario.version})`, + { concurrent: false }, + () => { + for (const mode of ["wrapped", "auto"] as const) { + defineCloudflareAgentsAssertions({ + name: `${mode} instrumentation`, + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { + CLOUDFLARE_AGENTS_INSTRUMENTATION_MODE: mode, + CLOUDFLARE_AGENTS_PACKAGE_NAME: scenario.dependencyName, + }, + runContext: { + cassette: false, + variantKey: scenario.variantKey, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.variantKey}-${mode}`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + } + }, + ); } }); diff --git a/e2e/scenarios/cloudflare-agents-instrumentation/worker.ts b/e2e/scenarios/cloudflare-agents-instrumentation/worker.ts index f02b0c156..c12dc7923 100644 --- a/e2e/scenarios/cloudflare-agents-instrumentation/worker.ts +++ b/e2e/scenarios/cloudflare-agents-instrumentation/worker.ts @@ -1,3 +1,4 @@ +// Bundler transforms and runtime consumers are enabled independently. import { Agent, getAgentByName } from "agents"; import { initLogger, wrapCloudflareAgent } from "braintrust/workerd"; @@ -18,6 +19,9 @@ type StoredRun = { status: "aborted" | "completed" | "error"; }; +if (__CLOUDFLARE_AGENTS_INSTRUMENTATION_MODE__ === "auto") + await import("braintrust/auto"); + const BaseParentAgent = __CLOUDFLARE_AGENTS_INSTRUMENTATION_MODE__ === "wrapped" ? wrapCloudflareAgent(Agent) diff --git a/e2e/scenarios/cloudflare-ai-chat-instrumentation/assertions.ts b/e2e/scenarios/cloudflare-ai-chat-instrumentation/assertions.ts index 93e078bbf..ce32465bc 100644 --- a/e2e/scenarios/cloudflare-ai-chat-instrumentation/assertions.ts +++ b/e2e/scenarios/cloudflare-ai-chat-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/cloudflare-ai-chat-instrumentation/scenario.test.ts b/e2e/scenarios/cloudflare-ai-chat-instrumentation/scenario.test.ts index 2da8bc9b6..25e879d1d 100644 --- a/e2e/scenarios/cloudflare-ai-chat-instrumentation/scenario.test.ts +++ b/e2e/scenarios/cloudflare-ai-chat-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -30,35 +30,39 @@ const scenarios = await Promise.all( })), ); -describe.sequential("Cloudflare AI Chat variants", () => { +describe("Cloudflare AI Chat variants", { concurrent: false }, () => { for (const scenario of scenarios) { - describe.sequential(`@cloudflare/ai-chat ${scenario.version}`, () => { - for (const mode of ["manual", "auto"] as const) { - defineCloudflareAIChatAssertions({ - name: `${mode} instrumentation`, - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - env: { - CLOUDFLARE_AI_CHAT_INSTRUMENTATION_MODE: mode, - CLOUDFLARE_AI_CHAT_PACKAGE_NAME: scenario.dependencyName, - }, - runContext: { - cassette: { + describe( + `@cloudflare/ai-chat ${scenario.version}`, + { concurrent: false }, + () => { + for (const mode of ["manual", "auto"] as const) { + defineCloudflareAIChatAssertions({ + name: `${mode} instrumentation`, + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { + CLOUDFLARE_AI_CHAT_INSTRUMENTATION_MODE: mode, + CLOUDFLARE_AI_CHAT_PACKAGE_NAME: scenario.dependencyName, + }, + runContext: { + cassette: { + variantKey: scenario.variantKey, + }, + originalScenarioDir, variantKey: scenario.variantKey, }, - originalScenarioDir, - variantKey: scenario.variantKey, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.variantKey}-${mode}`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - } - }); + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.variantKey}-${mode}`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + } + }, + ); } }); diff --git a/e2e/scenarios/cloudflare-ai-chat-instrumentation/worker.ts b/e2e/scenarios/cloudflare-ai-chat-instrumentation/worker.ts index 6d9eee970..13a5e4a9d 100644 --- a/e2e/scenarios/cloudflare-ai-chat-instrumentation/worker.ts +++ b/e2e/scenarios/cloudflare-ai-chat-instrumentation/worker.ts @@ -1,3 +1,4 @@ +// Bundler transforms and runtime consumers are enabled independently. import * as aiNamespace from "ai"; import * as aiChatNamespace from "@cloudflare/ai-chat"; import { createOpenAI } from "@ai-sdk/openai"; @@ -13,6 +14,8 @@ declare const __CLOUDFLARE_AI_CHAT_MODE__: "auto" | "manual"; declare const __OPENAI_API_KEY__: string; declare const __OPENAI_BASE_URL__: string; +if (__CLOUDFLARE_AI_CHAT_MODE__ === "auto") await import("braintrust/auto"); + const SCENARIO_NAME = "cloudflare-ai-chat-instrumentation"; const SUCCESS_ROOT_NAME = "cloudflare-ai-chat-success-root"; const ERROR_ROOT_NAME = "cloudflare-ai-chat-error-root"; diff --git a/e2e/scenarios/cloudflare-think-instrumentation/assertions.ts b/e2e/scenarios/cloudflare-think-instrumentation/assertions.ts index f6b6d3db4..da984d526 100644 --- a/e2e/scenarios/cloudflare-think-instrumentation/assertions.ts +++ b/e2e/scenarios/cloudflare-think-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/cloudflare-think-instrumentation/scenario.test.ts b/e2e/scenarios/cloudflare-think-instrumentation/scenario.test.ts index 19d3a1101..b5f3bf065 100644 --- a/e2e/scenarios/cloudflare-think-instrumentation/scenario.test.ts +++ b/e2e/scenarios/cloudflare-think-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -35,34 +35,38 @@ const variants = await Promise.all( const describeVariants = process.env.BRAINTRUST_E2E_CASSETTE_MODE === "record" || process.env.BRAINTRUST_E2E_CASSETTE_MODE === "record-missing" - ? describe.sequential + ? describe : describe.concurrent; describeVariants("Cloudflare Think variants", () => { for (const variant of variants) { - describe.sequential(`${variant.label} (${variant.version})`, () => { - for (const mode of ["manual", "auto"] as const) { - defineCloudflareThinkAssertions({ - name: `${mode} Workers/Vite instrumentation`, - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - env: { - CLOUDFLARE_THINK_INSTRUMENTATION: mode, - CLOUDFLARE_THINK_PACKAGE_NAME: variant.dependencyName, - }, - runContext: { - originalScenarioDir, - variantKey: `${variant.variantKey}-${mode}`, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: `${variant.variantKey}-${mode}`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - } - }); + describe( + `${variant.label} (${variant.version})`, + { concurrent: false }, + () => { + for (const mode of ["manual", "auto"] as const) { + defineCloudflareThinkAssertions({ + name: `${mode} Workers/Vite instrumentation`, + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + env: { + CLOUDFLARE_THINK_INSTRUMENTATION: mode, + CLOUDFLARE_THINK_PACKAGE_NAME: variant.dependencyName, + }, + runContext: { + originalScenarioDir, + variantKey: `${variant.variantKey}-${mode}`, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: `${variant.variantKey}-${mode}`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + } + }, + ); } }); diff --git a/e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts b/e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts index 155edbf3f..4d509747c 100644 --- a/e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts +++ b/e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts @@ -1,9 +1,13 @@ +// Bundler transforms and runtime consumers are enabled independently. import * as cloudflareThink from "@cloudflare/think"; import { createOpenAI } from "@ai-sdk/openai"; import { flush, initLogger, wrapCloudflareThink } from "braintrust"; import { tool } from "ai"; import { z } from "zod"; +if (process.env.CLOUDFLARE_THINK_INSTRUMENTATION !== "manual") + await import("braintrust/auto"); + const OPENAI_MODEL = "gpt-5-nano"; type Env = { diff --git a/e2e/scenarios/cohere-instrumentation/assertions.ts b/e2e/scenarios/cohere-instrumentation/assertions.ts index bf16ba31d..c1d51fdc7 100644 --- a/e2e/scenarios/cohere-instrumentation/assertions.ts +++ b/e2e/scenarios/cohere-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/cohere-instrumentation/scenario.test.ts b/e2e/scenarios/cohere-instrumentation/scenario.test.ts index 9654cfb70..da0396ffb 100644 --- a/e2e/scenarios/cohere-instrumentation/scenario.test.ts +++ b/e2e/scenarios/cohere-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -29,7 +29,7 @@ describe.concurrent("variants", () => { for (const scenario of cohereScenarios) { const supportsThinking = scenario.supportsThinking ?? true; - describe.sequential(`cohere sdk ${scenario.version}`, () => { + describe(`cohere sdk ${scenario.version}`, { concurrent: false }, () => { defineCohereInstrumentationAssertions({ name: "wrapped instrumentation", runScenario: async ({ runScenarioDir }) => { diff --git a/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts b/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts index 766853427..884e55c31 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts +++ b/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/cursor-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/cursor-sdk-instrumentation/scenario.test.ts index 2e05d810e..cb72b2f55 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/cursor-sdk-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -40,45 +40,53 @@ const cursorSDKScenarios = await Promise.all( describe.concurrent("variants", () => { for (const cursorSDKScenario of cursorSDKScenarios) { - describe.sequential(`cursor sdk ${cursorSDKScenario.version}`, () => { - defineCursorSDKInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: cursorSDKScenario.wrapperEntry, - env: { CURSOR_SDK_PACKAGE_NAME: cursorSDKScenario.dependencyName }, - runContext: { - variantKey: cursorSDKScenario.variantKey, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: cursorSDKScenario.wrapperSnapshotName, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); + describe( + `cursor sdk ${cursorSDKScenario.version}`, + { concurrent: false }, + () => { + defineCursorSDKInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: cursorSDKScenario.wrapperEntry, + env: { + CURSOR_SDK_PACKAGE_NAME: cursorSDKScenario.dependencyName, + }, + runContext: { + variantKey: cursorSDKScenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: cursorSDKScenario.wrapperSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); - defineCursorSDKInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: cursorSDKScenario.autoEntry, - env: { CURSOR_SDK_PACKAGE_NAME: cursorSDKScenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: cursorSDKScenario.variantKey, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: cursorSDKScenario.autoSnapshotName, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + defineCursorSDKInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: cursorSDKScenario.autoEntry, + env: { + CURSOR_SDK_PACKAGE_NAME: cursorSDKScenario.dependencyName, + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: cursorSDKScenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: cursorSDKScenario.autoSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/deepseek-harness-instrumentation/scenario.test.ts b/e2e/scenarios/deepseek-harness-instrumentation/scenario.test.ts index 741b1cb58..4ba8f173f 100644 --- a/e2e/scenarios/deepseek-harness-instrumentation/scenario.test.ts +++ b/e2e/scenarios/deepseek-harness-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { @@ -41,124 +41,130 @@ const scenarios = await Promise.all( ); const TIMEOUT_MS = 120_000; -describe.sequential("DeepSeek Harness instrumentation variants", () => { - for (const scenario of scenarios) { - describe(`${scenario.label} (${scenario.version})`, () => { - let events: CapturedLogEvent[] = []; +describe( + "DeepSeek Harness instrumentation variants", + { concurrent: false }, + () => { + for (const scenario of scenarios) { + describe(`${scenario.label} (${scenario.version})`, () => { + let events: CapturedLogEvent[] = []; - beforeAll(async () => { - await withScenarioHarness( - async ({ events: harnessEvents, runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: { - DEEPSEEK_HARNESS_PACKAGE: scenario.dependencyName, - NODE_ENV: "development", - }, - runContext: { - originalScenarioDir, - variantKey: scenario.variantKey, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - events = harnessEvents(); - }, - ); - }, TIMEOUT_MS); + beforeAll(async () => { + await withScenarioHarness( + async ({ events: harnessEvents, runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { + DEEPSEEK_HARNESS_PACKAGE: scenario.dependencyName, + NODE_ENV: "development", + }, + runContext: { + originalScenarioDir, + variantKey: scenario.variantKey, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + events = harnessEvents(); + }, + ); + }, TIMEOUT_MS); + + test("captures each user turn as an independent trace", async () => { + const turns = findAllSpans(events, "deepseek_harness.turn"); + const first = turns.find( + (turn) => + Array.isArray(turn.input) && + turn.input[0]?.content === + "Call lookup exactly once with query `first`. Then reply exactly: First Harness answer.", + ); + const second = turns.find( + (turn) => + Array.isArray(turn.input) && + turn.input[0]?.content === + "Call lookup exactly once with query `second`. Then reply exactly: Second Harness answer.", + ); + + expect(turns).toHaveLength(2); + expect(first?.span.type).toBe("task"); + expect(second?.span.type).toBe("task"); + expect(first?.span.parentIds).toEqual([]); + expect(second?.span.parentIds).toEqual([]); + expect(first?.span.rootId).not.toEqual(second?.span.rootId); + expect(first?.metadata?.["deepseek_harness.session_id"]).toEqual( + second?.metadata?.["deepseek_harness.session_id"], + ); + expect(first?.metadata?.scenario).toBe( + "deepseek-harness-instrumentation", + ); + expect(first?.metadata?.testRunId).toMatch(/^e2e-/); + expect(second?.metadata?.scenario).toBe( + "deepseek-harness-instrumentation", + ); + expect(second?.metadata?.testRunId).toBe(first?.metadata?.testRunId); + expect(first?.output).toBe("First Harness answer."); + expect(second?.output).toBe("Second Harness answer."); - test("captures each user turn as an independent trace", async () => { - const turns = findAllSpans(events, "deepseek_harness.turn"); - const first = turns.find( - (turn) => - Array.isArray(turn.input) && - turn.input[0]?.content === - "Call lookup exactly once with query `first`. Then reply exactly: First Harness answer.", - ); - const second = turns.find( - (turn) => - Array.isArray(turn.input) && - turn.input[0]?.content === - "Call lookup exactly once with query `second`. Then reply exactly: Second Harness answer.", - ); + for (const root of [first, second]) { + const steps = findChildSpans( + events, + "deepseek_harness.step", + root?.span.id, + ); + const lookup = findLatestChildSpan(events, "lookup", root?.span.id); + expect(steps).toHaveLength(2); + expect(steps.map((step) => step.span.type)).toEqual(["llm", "llm"]); + expect(steps.map((step) => step.metadata?.provider)).toEqual([ + "openai", + "openai", + ]); + expect(steps.map((step) => step.metadata?.model)).toEqual([ + "gpt-5.6-luna", + "gpt-5.6-luna", + ]); + expect(steps[0]?.output).toMatchObject([ + { + finish_reason: "tool_calls", + message: { + tool_calls: [ + { function: { name: "lookup" }, type: "function" }, + ], + }, + }, + ]); + expect(lookup?.span.type).toBe("tool"); + expect(lookup?.span.parentIds).toEqual([root?.span.id]); + expect(lookup?.output).toEqual( + expect.stringContaining("Lookup result"), + ); + expect(root?.metrics?.tokens).toEqual(expect.any(Number)); + expect(root?.metrics?.prompt_tokens).toEqual(expect.any(Number)); + expect(root?.metrics?.completion_tokens).toEqual( + expect.any(Number), + ); + } - expect(turns).toHaveLength(2); - expect(first?.span.type).toBe("task"); - expect(second?.span.type).toBe("task"); - expect(first?.span.parentIds).toEqual([]); - expect(second?.span.parentIds).toEqual([]); - expect(first?.span.rootId).not.toEqual(second?.span.rootId); - expect(first?.metadata?.["deepseek_harness.session_id"]).toEqual( - second?.metadata?.["deepseek_harness.session_id"], - ); - expect(first?.metadata?.scenario).toBe( - "deepseek-harness-instrumentation", - ); - expect(first?.metadata?.testRunId).toMatch(/^e2e-/); - expect(second?.metadata?.scenario).toBe( - "deepseek-harness-instrumentation", - ); - expect(second?.metadata?.testRunId).toBe(first?.metadata?.testRunId); - expect(first?.output).toBe("First Harness answer."); - expect(second?.output).toBe("Second Harness answer."); + for (const event of events) { + expect(spanInstrumentationName(event)).toBe("deepseek-harness"); + } - for (const root of [first, second]) { - const steps = findChildSpans( + await matchSpanTreeSnapshot( events, - "deepseek_harness.step", - root?.span.id, - ); - const lookup = findLatestChildSpan(events, "lookup", root?.span.id); - expect(steps).toHaveLength(2); - expect(steps.map((step) => step.span.type)).toEqual(["llm", "llm"]); - expect(steps.map((step) => step.metadata?.provider)).toEqual([ - "openai", - "openai", - ]); - expect(steps.map((step) => step.metadata?.model)).toEqual([ - "gpt-5.6-luna", - "gpt-5.6-luna", - ]); - expect(steps[0]?.output).toMatchObject([ + resolveFileSnapshotPath( + import.meta.url, + `${scenario.variantKey}.span-tree.json`, + ), { - finish_reason: "tool_calls", - message: { - tool_calls: [ - { function: { name: "lookup" }, type: "function" }, + normalize: { + additionalProviderIdKeys: [ + "deepseek_harness.call_id", + "deepseek_harness.session_id", ], }, }, - ]); - expect(lookup?.span.type).toBe("tool"); - expect(lookup?.span.parentIds).toEqual([root?.span.id]); - expect(lookup?.output).toEqual( - expect.stringContaining("Lookup result"), ); - expect(root?.metrics?.tokens).toEqual(expect.any(Number)); - expect(root?.metrics?.prompt_tokens).toEqual(expect.any(Number)); - expect(root?.metrics?.completion_tokens).toEqual(expect.any(Number)); - } - - for (const event of events) { - expect(spanInstrumentationName(event)).toBe("deepseek-harness"); - } - - await matchSpanTreeSnapshot( - events, - resolveFileSnapshotPath( - import.meta.url, - `${scenario.variantKey}.span-tree.json`, - ), - { - normalize: { - additionalProviderIdKeys: [ - "deepseek_harness.call_id", - "deepseek_harness.session_id", - ], - }, - }, - ); + }); }); - }); - } -}); + } + }, +); diff --git a/e2e/scenarios/deno-browser/runner.case.ts b/e2e/scenarios/deno-browser/runner.case.ts index 823b53dc6..6cf767321 100644 --- a/e2e/scenarios/deno-browser/runner.case.ts +++ b/e2e/scenarios/deno-browser/runner.case.ts @@ -45,12 +45,9 @@ Deno.test("deno-browser exposes the expected browser runtime surface", () => { expectNunjucksTemplateUnavailable(braintrust); }); -Deno.test( - "deno-browser can run Eval locally without sending logs", - async () => { - await expectEvalWorks(braintrust); - }, -); +Deno.test("deno-browser can run Eval locally without sending logs", async () => { + await expectEvalWorks(braintrust); +}); Deno.test("deno-browser emits trace payloads over HTTP", async () => { const logger = createLogger(); diff --git a/e2e/scenarios/deno-browser/scenario.test.ts b/e2e/scenarios/deno-browser/scenario.test.ts index eb066f2d1..87b993cc8 100644 --- a/e2e/scenarios/deno-browser/scenario.test.ts +++ b/e2e/scenarios/deno-browser/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, matchFileSnapshot, diff --git a/e2e/scenarios/deno-node/scenario.test.ts b/e2e/scenarios/deno-node/scenario.test.ts index 9fa8b34f2..fd24abe51 100644 --- a/e2e/scenarios/deno-node/scenario.test.ts +++ b/e2e/scenarios/deno-node/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, matchFileSnapshot, diff --git a/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts b/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts index 4dbecef9f..4543c1e43 100644 --- a/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts +++ b/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "vite-plus/test"; import { prepareScenarioDir, resolveScenarioDir, @@ -16,7 +16,7 @@ const scenarioDir = await prepareScenarioDir({ scenarioDir: originalScenarioDir, }); for (const variant of ["elevenlabs-v2", "elevenlabs-v2-latest"]) { - describe.sequential(variant, () => { + describe(variant, { concurrent: false }, () => { for (const mode of ["wrapped", "auto", "wrapped-auto"]) { test( mode, diff --git a/e2e/scenarios/eve-instrumentation/scenario.test.ts b/e2e/scenarios/eve-instrumentation/scenario.test.ts index 1855d6496..f37280339 100644 --- a/e2e/scenarios/eve-instrumentation/scenario.test.ts +++ b/e2e/scenarios/eve-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import type { CapturedLogEvent, @@ -64,7 +64,7 @@ const eveScenarios = await Promise.all( const TIMEOUT_MS = process.env.BRAINTRUST_E2E_CASSETTE_MODE === "record" ? 300_000 : 120_000; -describe.sequential("eve instrumentation variants", () => { +describe("eve instrumentation variants", { concurrent: false }, () => { for (const scenario of eveScenarios) { describe(`${scenario.label} (${scenario.version})`, () => { let events: CapturedLogEvent[] = []; diff --git a/e2e/scenarios/flue-instrumentation/assertions.ts b/e2e/scenarios/flue-instrumentation/assertions.ts index 89b564836..dfb7afbe6 100644 --- a/e2e/scenarios/flue-instrumentation/assertions.ts +++ b/e2e/scenarios/flue-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { @@ -200,7 +200,7 @@ export function defineFlueInstrumentationAssertions(options: { const timeoutMs = effectiveScenarioTimeoutMs(options.timeoutMs); const testConfig = { timeout: timeoutMs }; - describe.sequential(options.name, () => { + describe(options.name, { concurrent: false }, () => { let events: CapturedLogEvent[] = []; beforeAll(async () => { diff --git a/e2e/scenarios/flue-instrumentation/scenario.test.ts b/e2e/scenarios/flue-instrumentation/scenario.test.ts index 22d9941be..0c9280c5d 100644 --- a/e2e/scenarios/flue-instrumentation/scenario.test.ts +++ b/e2e/scenarios/flue-instrumentation/scenario.test.ts @@ -1,6 +1,6 @@ import { promises as fs } from "node:fs"; import path from "node:path"; -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -50,41 +50,49 @@ const flueV2Scenarios = await Promise.all( })), ); -describe.sequential("flue variants", () => { +describe("flue variants", { concurrent: false }, () => { for (const scenario of flueScenarios) { - describe.sequential(`flue ${scenario.label} (${scenario.version})`, () => { - defineFlueInstrumentationAssertions({ - name: "explicit instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: scenario.env, - runContext: { - originalScenarioDir, - variantKey: scenario.variantKey, - }, - scenarioDir: scenario.scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.variantKey}-explicit`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + describe( + `flue ${scenario.label} (${scenario.version})`, + { concurrent: false }, + () => { + defineFlueInstrumentationAssertions({ + name: "explicit instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: scenario.env, + runContext: { + originalScenarioDir, + variantKey: scenario.variantKey, + }, + scenarioDir: scenario.scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.variantKey}-explicit`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } for (const scenario of flueV2Scenarios) { - describe.sequential(`flue ${scenario.label} (${scenario.version})`, () => { - defineFlueV2InstrumentationAssertions({ - originalScenarioDir, - runtimePackageName: scenario.dependencyName, - scenarioDir: flueV2ScenarioDir, - snapshotName: scenario.variantKey, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + describe( + `flue ${scenario.label} (${scenario.version})`, + { concurrent: false }, + () => { + defineFlueV2InstrumentationAssertions({ + originalScenarioDir, + runtimePackageName: scenario.dependencyName, + scenarioDir: flueV2ScenarioDir, + snapshotName: scenario.variantKey, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/flue-instrumentation/v2-assertions.ts b/e2e/scenarios/flue-instrumentation/v2-assertions.ts index 92f69426f..b6e9d0f62 100644 --- a/e2e/scenarios/flue-instrumentation/v2-assertions.ts +++ b/e2e/scenarios/flue-instrumentation/v2-assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { @@ -34,7 +34,7 @@ export function defineFlueV2InstrumentationAssertions(options: { `${options.snapshotName}.span-tree.json`, ); - describe.sequential("explicit native instrumentation", () => { + describe("explicit native instrumentation", { concurrent: false }, () => { let events: CapturedLogEvent[] = []; let testRunId: string; diff --git a/e2e/scenarios/genkit-instrumentation/assertions.ts b/e2e/scenarios/genkit-instrumentation/assertions.ts index aff41a00d..9f26ba072 100644 --- a/e2e/scenarios/genkit-instrumentation/assertions.ts +++ b/e2e/scenarios/genkit-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/genkit-instrumentation/scenario.test.ts b/e2e/scenarios/genkit-instrumentation/scenario.test.ts index 97ad01d1c..302920a6f 100644 --- a/e2e/scenarios/genkit-instrumentation/scenario.test.ts +++ b/e2e/scenarios/genkit-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -34,7 +34,7 @@ const genkitScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of genkitScenarios) { - describe.sequential(`genkit ${scenario.version}`, () => { + describe(`genkit ${scenario.version}`, { concurrent: false }, () => { defineGenkitInstrumentationAssertions({ name: "wrapped instrumentation", runScenario: async ({ runScenarioDir }) => { diff --git a/e2e/scenarios/git-metadata/scenario.test.ts b/e2e/scenarios/git-metadata/scenario.test.ts index 14f3036f9..8c0179df0 100644 --- a/e2e/scenarios/git-metadata/scenario.test.ts +++ b/e2e/scenarios/git-metadata/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { prepareScenarioDir, resolveScenarioDir, diff --git a/e2e/scenarios/github-copilot-instrumentation/assertions.ts b/e2e/scenarios/github-copilot-instrumentation/assertions.ts index 14812767d..5183d6366 100644 --- a/e2e/scenarios/github-copilot-instrumentation/assertions.ts +++ b/e2e/scenarios/github-copilot-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/github-copilot-instrumentation/scenario.test.ts b/e2e/scenarios/github-copilot-instrumentation/scenario.test.ts index 560d08d67..883358774 100644 --- a/e2e/scenarios/github-copilot-instrumentation/scenario.test.ts +++ b/e2e/scenarios/github-copilot-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -32,45 +32,49 @@ const githubCopilotScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of githubCopilotScenarios) { - describe.sequential(`github copilot sdk ${scenario.version}`, () => { - defineGitHubCopilotInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: { GITHUB_COPILOT_PACKAGE_NAME: scenario.dependencyName }, - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: GITHUB_COPILOT_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-wrapped`, - testFileUrl: import.meta.url, - timeoutMs: GITHUB_COPILOT_SCENARIO_TIMEOUT_MS, - }); + describe( + `github copilot sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineGitHubCopilotInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { GITHUB_COPILOT_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: GITHUB_COPILOT_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: GITHUB_COPILOT_SCENARIO_TIMEOUT_MS, + }); - defineGitHubCopilotInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - env: { GITHUB_COPILOT_PACKAGE_NAME: scenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: GITHUB_COPILOT_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-auto`, - testFileUrl: import.meta.url, - timeoutMs: GITHUB_COPILOT_SCENARIO_TIMEOUT_MS, - }); - }); + defineGitHubCopilotInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { GITHUB_COPILOT_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: GITHUB_COPILOT_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-auto`, + testFileUrl: import.meta.url, + timeoutMs: GITHUB_COPILOT_SCENARIO_TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/google-adk-instrumentation/assertions.ts b/e2e/scenarios/google-adk-instrumentation/assertions.ts index c2e484b3e..53e569967 100644 --- a/e2e/scenarios/google-adk-instrumentation/assertions.ts +++ b/e2e/scenarios/google-adk-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { Json } from "../../helpers/normalize"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; diff --git a/e2e/scenarios/google-adk-instrumentation/scenario.test.ts b/e2e/scenarios/google-adk-instrumentation/scenario.test.ts index 74e2a90d5..8259d675c 100644 --- a/e2e/scenarios/google-adk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/google-adk-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -48,47 +48,51 @@ const googleADKScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of googleADKScenarios) { - describe.sequential(`google adk sdk ${scenario.version}`, () => { - defineGoogleADKInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: scenario.wrapperEntry, - env: { GOOGLE_ADK_PACKAGE_NAME: scenario.dependencyName }, - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - expectLLMSpan: false, - snapshotName: `${scenario.snapshotName}-wrapped`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); + describe( + `google adk sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineGoogleADKInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: scenario.wrapperEntry, + env: { GOOGLE_ADK_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + expectLLMSpan: false, + snapshotName: `${scenario.snapshotName}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); - defineGoogleADKInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: scenario.autoEntry, - env: { GOOGLE_ADK_PACKAGE_NAME: scenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - expectLLMSpan: true, - snapshotName: `${scenario.snapshotName}-auto-hook`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + defineGoogleADKInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: scenario.autoEntry, + env: { GOOGLE_ADK_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + expectLLMSpan: true, + snapshotName: `${scenario.snapshotName}-auto-hook`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/google-genai-instrumentation/assertions.ts b/e2e/scenarios/google-genai-instrumentation/assertions.ts index 2f4d3ec63..3ddc9ea82 100644 --- a/e2e/scenarios/google-genai-instrumentation/assertions.ts +++ b/e2e/scenarios/google-genai-instrumentation/assertions.ts @@ -2,7 +2,7 @@ import { EMBEDDING_DIMENSIONS, GOOGLE_MULTIMODAL_EMBEDDING_MODEL, } from "./embeddings.mjs"; -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { Json } from "../../helpers/normalize"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; diff --git a/e2e/scenarios/google-genai-instrumentation/scenario.test.ts b/e2e/scenarios/google-genai-instrumentation/scenario.test.ts index 07bff02e8..51fef3f1e 100644 --- a/e2e/scenarios/google-genai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/google-genai-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -48,8 +48,9 @@ const googleGenAIScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of googleGenAIScenarios) { - describe.sequential( + describe( `${scenario.snapshotName}: google genai sdk ${scenario.version}`, + { concurrent: false }, () => { defineGoogleGenAIInstrumentationAssertions({ name: "wrapped instrumentation", diff --git a/e2e/scenarios/google-generative-ai-instrumentation/assertions.ts b/e2e/scenarios/google-generative-ai-instrumentation/assertions.ts index 2d05ff9f6..1c6f71cb0 100644 --- a/e2e/scenarios/google-generative-ai-instrumentation/assertions.ts +++ b/e2e/scenarios/google-generative-ai-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { effectiveScenarioTimeoutMs, diff --git a/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts b/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts index 3d24ba178..9b5845db7 100644 --- a/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -24,51 +24,55 @@ const scenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of scenarios) { - describe.sequential(`google generative ai sdk ${scenario.version}`, () => { - defineGoogleGenerativeAIInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: { - GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName, - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", - }, - runContext: { - originalScenarioDir, - variantKey: scenario.snapshotName, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.snapshotName, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); + describe( + `google generative ai sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineGoogleGenerativeAIInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { + GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName, + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + }, + runContext: { + originalScenarioDir, + variantKey: scenario.snapshotName, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.snapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); - defineGoogleGenerativeAIInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - nodeArgs: ["--import", "braintrust/hook.mjs"], - env: { - GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName, - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", - }, - runContext: { - originalScenarioDir, - variantKey: scenario.snapshotName, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.snapshotName, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + defineGoogleGenerativeAIInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + nodeArgs: ["--import", "braintrust/hook.mjs"], + env: { + GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName, + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + }, + runContext: { + originalScenarioDir, + variantKey: scenario.snapshotName, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.snapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/groq-instrumentation/assertions.ts b/e2e/scenarios/groq-instrumentation/assertions.ts index c9b997d40..edbc5fd8b 100644 --- a/e2e/scenarios/groq-instrumentation/assertions.ts +++ b/e2e/scenarios/groq-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/groq-instrumentation/scenario.test.ts b/e2e/scenarios/groq-instrumentation/scenario.test.ts index 10641bbbc..4ba9b68a1 100644 --- a/e2e/scenarios/groq-instrumentation/scenario.test.ts +++ b/e2e/scenarios/groq-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { resolve } from "node:path"; import { prepareScenarioDir, @@ -37,7 +37,7 @@ const groqScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of groqScenarios) { - describe.sequential(`groq sdk ${scenario.version}`, () => { + describe(`groq sdk ${scenario.version}`, { concurrent: false }, () => { defineGroqInstrumentationAssertions({ name: "wrapped instrumentation", runScenario: async ({ runScenarioDir }) => { diff --git a/e2e/scenarios/huggingface-instrumentation/assertions.ts b/e2e/scenarios/huggingface-instrumentation/assertions.ts index b6bc1c5dc..393c161bb 100644 --- a/e2e/scenarios/huggingface-instrumentation/assertions.ts +++ b/e2e/scenarios/huggingface-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import { type Json } from "../../helpers/normalize"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; diff --git a/e2e/scenarios/huggingface-instrumentation/scenario.test.ts b/e2e/scenarios/huggingface-instrumentation/scenario.test.ts index 8302c4ea3..d8e59b2dc 100644 --- a/e2e/scenarios/huggingface-instrumentation/scenario.test.ts +++ b/e2e/scenarios/huggingface-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -27,47 +27,51 @@ const huggingFaceScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of huggingFaceScenarios) { - describe.sequential(`huggingface inference sdk ${scenario.version}`, () => { - defineHuggingFaceInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: scenario.wrapperEntry, - env: { HUGGINGFACE_PACKAGE_NAME: scenario.dependencyName }, - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: HUGGINGFACE_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-wrapped`, - supportsToolCalls: false, - testFileUrl: import.meta.url, - timeoutMs: HUGGINGFACE_SCENARIO_TIMEOUT_MS, - }); + describe( + `huggingface inference sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineHuggingFaceInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: scenario.wrapperEntry, + env: { HUGGINGFACE_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: HUGGINGFACE_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-wrapped`, + supportsToolCalls: false, + testFileUrl: import.meta.url, + timeoutMs: HUGGINGFACE_SCENARIO_TIMEOUT_MS, + }); - defineHuggingFaceInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: scenario.autoEntry, - env: { HUGGINGFACE_PACKAGE_NAME: scenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: HUGGINGFACE_SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-auto-hook`, - supportsToolCalls: scenario.supportsToolCalls, - testFileUrl: import.meta.url, - timeoutMs: HUGGINGFACE_SCENARIO_TIMEOUT_MS, - }); - }); + defineHuggingFaceInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: scenario.autoEntry, + env: { HUGGINGFACE_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: HUGGINGFACE_SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-auto-hook`, + supportsToolCalls: scenario.supportsToolCalls, + testFileUrl: import.meta.url, + timeoutMs: HUGGINGFACE_SCENARIO_TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/huggingface-transformers-instrumentation/assertions.ts b/e2e/scenarios/huggingface-transformers-instrumentation/assertions.ts index 76a477798..330a17cb5 100644 --- a/e2e/scenarios/huggingface-transformers-instrumentation/assertions.ts +++ b/e2e/scenarios/huggingface-transformers-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/huggingface-transformers-instrumentation/scenario.test.ts b/e2e/scenarios/huggingface-transformers-instrumentation/scenario.test.ts index db603b068..537eb1550 100644 --- a/e2e/scenarios/huggingface-transformers-instrumentation/scenario.test.ts +++ b/e2e/scenarios/huggingface-transformers-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -23,56 +23,36 @@ const scenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of scenarios) { - describe.sequential(`Transformers.js ${scenario.version}`, () => { - defineAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.wrapper.ts", - env: { - HUGGINGFACE_TRANSFORMERS_PACKAGE_NAME: scenario.dependencyName, - }, - runContext: { - originalScenarioDir, - variantKey: scenario.snapshotName, - }, - scenarioDir, - timeoutMs: SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-wrapped`, - testFileUrl: import.meta.url, - timeoutMs: SCENARIO_TIMEOUT_MS, - }); - - defineAssertions({ - name: "auto-hook instrumentation ESM", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.auto.mjs", - env: { - HUGGINGFACE_TRANSFORMERS_PACKAGE_NAME: scenario.dependencyName, - }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - originalScenarioDir, - variantKey: scenario.snapshotName, - }, - scenarioDir, - timeoutMs: SCENARIO_TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.snapshotName}-auto-esm`, - testFileUrl: import.meta.url, - timeoutMs: SCENARIO_TIMEOUT_MS, - }); + describe( + `Transformers.js ${scenario.version}`, + { concurrent: false }, + () => { + defineAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.wrapper.ts", + env: { + HUGGINGFACE_TRANSFORMERS_PACKAGE_NAME: scenario.dependencyName, + }, + runContext: { + originalScenarioDir, + variantKey: scenario.snapshotName, + }, + scenarioDir, + timeoutMs: SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: SCENARIO_TIMEOUT_MS, + }); - if (scenario.runCjsAutoInstrumentation) { defineAssertions({ - name: "auto-hook instrumentation CJS", + name: "auto-hook instrumentation ESM", runScenario: async ({ runNodeScenarioDir }) => { await runNodeScenarioDir({ - entry: "scenario.auto.cjs", + entry: "scenario.auto.mjs", env: { HUGGINGFACE_TRANSFORMERS_PACKAGE_NAME: scenario.dependencyName, }, @@ -85,11 +65,36 @@ describe.concurrent("variants", () => { timeoutMs: SCENARIO_TIMEOUT_MS, }); }, - snapshotName: `${scenario.snapshotName}-auto-cjs`, + snapshotName: `${scenario.snapshotName}-auto-esm`, testFileUrl: import.meta.url, timeoutMs: SCENARIO_TIMEOUT_MS, }); - } - }); + + if (scenario.runCjsAutoInstrumentation) { + defineAssertions({ + name: "auto-hook instrumentation CJS", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.auto.cjs", + env: { + HUGGINGFACE_TRANSFORMERS_PACKAGE_NAME: + scenario.dependencyName, + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + originalScenarioDir, + variantKey: scenario.snapshotName, + }, + scenarioDir, + timeoutMs: SCENARIO_TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotName}-auto-cjs`, + testFileUrl: import.meta.url, + timeoutMs: SCENARIO_TIMEOUT_MS, + }); + } + }, + ); } }); diff --git a/e2e/scenarios/langchain-anthropic/scenario.test.ts b/e2e/scenarios/langchain-anthropic/scenario.test.ts index 2b312ae20..29101ba3a 100644 --- a/e2e/scenarios/langchain-anthropic/scenario.test.ts +++ b/e2e/scenarios/langchain-anthropic/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { prepareScenarioDir, diff --git a/e2e/scenarios/langgraph-auto-instrumentation/assertions.ts b/e2e/scenarios/langgraph-auto-instrumentation/assertions.ts index cd94843e8..0c9996f16 100644 --- a/e2e/scenarios/langgraph-auto-instrumentation/assertions.ts +++ b/e2e/scenarios/langgraph-auto-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { expect } from "vitest"; +import { expect } from "vite-plus/test"; import { normalizeForSnapshot, type Json } from "../../helpers/normalize"; import type { CapturedLogEvent, diff --git a/e2e/scenarios/langgraph-auto-instrumentation/scenario.test.ts b/e2e/scenarios/langgraph-auto-instrumentation/scenario.test.ts index 4b4997d02..2c4a3df67 100644 --- a/e2e/scenarios/langgraph-auto-instrumentation/scenario.test.ts +++ b/e2e/scenarios/langgraph-auto-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, resolveFileSnapshotPath, diff --git a/e2e/scenarios/langgraph-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/langgraph-sdk-instrumentation/scenario.test.ts index 03bb88480..dcc56cac0 100644 --- a/e2e/scenarios/langgraph-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/langgraph-sdk-instrumentation/scenario.test.ts @@ -3,7 +3,7 @@ import { findChildSpans, findLatestSpan, } from "../../helpers/trace-selectors"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -26,276 +26,282 @@ const variants = await Promise.all( describe.concurrent("variants", () => { for (const { dependency, version } of variants) { - describe.sequential(`LangGraph SDK ${version} (${dependency})`, () => { - for (const module of ["esm", "cjs"]) { - for (const mode of ["wrapped", "auto", "both", "disabled"]) { - it(`${module} ${mode}`, async () => { - await withScenarioHarness( - async (harness) => { - const result = await harness.runNodeScenarioDir({ - scenarioDir, - entry: "scenario.mjs", - timeoutMs: 120_000, - env: { - LANGGRAPH_SDK_PACKAGE: dependency, - LANGGRAPH_SDK_VERSION: version, - LANGGRAPH_SDK_MODULE: module, - LANGGRAPH_SDK_MODE: mode, - ...(mode === "disabled" - ? { BRAINTRUST_DISABLE_INSTRUMENTATION: "langgraph-sdk" } - : {}), - }, - nodeArgs: - mode === "wrapped" - ? [] - : ["--import", "braintrust/hook.mjs"], - // The real server and graph run in every lane. Only model HTTP - // responses are recorded and replayed by the harness. - runContext: { originalScenarioDir, variantKey: dependency }, - }); - const usageLine = result.stdout - .split("\n") - .find((line) => - line.startsWith("LANGGRAPH_EXPECTED_USAGE "), - )!; - const expectedUsage = JSON.parse( - usageLine.slice("LANGGRAPH_EXPECTED_USAGE ".length), - ); - const rawEvents = harness.events(); - const events = [ - ...new Set(rawEvents.map((event) => event.span.name)), - ].flatMap((name) => - name ? findAllSpans(rawEvents, name) : [], - ); - const instrumented = events.filter((event) => - event.span.name?.startsWith("langgraph.runs."), - ); - const root = events.find( - (event) => - event.metadata?.scenario === - "langgraph-sdk-instrumentation", - )!; - expect(root.metadata).toMatchObject({ - sdk_version: version, - module, - instrumentation_mode: mode, - expected_error_cases: 4, - }); - expect(root.output).toEqual({ - status: "passed", - expected_error_cases: 4, - }); - expect(events.every((event) => event.span.ended)).toBe(true); - const operationSpecs = [ - [ - "background", - "Uninstrumented background APIs", - "background-apis", - ], - ["wait", "Wait for final result", "wait"], - [ - "interruptResume", - "Interrupt and resume", - "interrupt-resume", - ], - ["values", "Stream values mode", "values"], - ["messages", "Stream messages mode", "messages"], - ["updates", "Stream updates mode", "updates"], - ["streamError", "Stream error", "stream-error"], - ["cancel", "Cancel stream", "cancel"], - [ - "missingAssistant", - "Missing assistant error", - "missing-assistant-error", - ], - ["thrownError", "Thrown graph error", "thrown-error"], - ["returnedError", "Returned graph error", "returned-error"], - ["concurrent", "Concurrent waits", "concurrent-waits"], - ] as const; - const operations = Object.fromEntries( - operationSpecs.map(([key, spanName, operation]) => { - const event = findLatestSpan(rawEvents, spanName)!; - expect(event.span.parentIds).toEqual([root.span.id]); - expect(event.metadata).toMatchObject({ operation }); - return [key, event]; - }), - ) as Record< - (typeof operationSpecs)[number][0], - (typeof events)[number] - >; - expect( - events.filter((event) => - event.span.parentIds.includes( - operations.background.span.id!, - ), - ), - ).toEqual([]); - if (mode === "disabled") { - expect(instrumented).toHaveLength(0); - expect(events).toHaveLength(13); - return; - } - expect(events).toHaveLength(26); - expect(instrumented).toHaveLength(13); - expect( - instrumented.filter((event) => event.row.error), - ).toHaveLength(4); - for (const event of instrumented) { - expect(event.span.type).toBe("task"); - expect(event.context?.span_origin).toMatchObject({ - instrumentation: { name: "langgraph-sdk" }, + describe( + `LangGraph SDK ${version} (${dependency})`, + { concurrent: false }, + () => { + for (const module of ["esm", "cjs"]) { + for (const mode of ["wrapped", "auto", "both", "disabled"]) { + it(`${module} ${mode}`, async () => { + await withScenarioHarness( + async (harness) => { + const result = await harness.runNodeScenarioDir({ + scenarioDir, + entry: "scenario.mjs", + timeoutMs: 120_000, + env: { + LANGGRAPH_SDK_PACKAGE: dependency, + LANGGRAPH_SDK_VERSION: version, + LANGGRAPH_SDK_MODULE: module, + LANGGRAPH_SDK_MODE: mode, + ...(mode === "disabled" + ? { + BRAINTRUST_DISABLE_INSTRUMENTATION: "langgraph-sdk", + } + : {}), + }, + nodeArgs: + mode === "wrapped" + ? [] + : ["--import", "braintrust/hook.mjs"], + // The real server and graph run in every lane. Only model HTTP + // responses are recorded and replayed by the harness. + runContext: { originalScenarioDir, variantKey: dependency }, }); - expect(event.span.parentIds).toHaveLength(1); - } - const wait = findChildSpans( - rawEvents, - "langgraph.runs.wait", - operations.wait.span.id, - ); - const interruptResume = findChildSpans( - rawEvents, - "langgraph.runs.wait", - operations.interruptResume.span.id, - ); - const values = findChildSpans( - rawEvents, - "langgraph.runs.stream", - operations.values.span.id, - ); - const messages = findChildSpans( - rawEvents, - "langgraph.runs.stream", - operations.messages.span.id, - ); - const updates = findChildSpans( - rawEvents, - "langgraph.runs.stream", - operations.updates.span.id, - ); - const streamError = findChildSpans( - rawEvents, - "langgraph.runs.stream", - operations.streamError.span.id, - ); - const cancel = findChildSpans( - rawEvents, - "langgraph.runs.stream", - operations.cancel.span.id, - ); - const missingAssistant = findChildSpans( - rawEvents, - "langgraph.runs.wait", - operations.missingAssistant.span.id, - ); - const thrownError = findChildSpans( - rawEvents, - "langgraph.runs.wait", - operations.thrownError.span.id, - ); - const returnedError = findChildSpans( - rawEvents, - "langgraph.runs.wait", - operations.returnedError.span.id, - ); - const concurrent = findChildSpans( - rawEvents, - "langgraph.runs.wait", - operations.concurrent.span.id, - ); - for (const group of [ - wait, - values, - messages, - updates, - streamError, - cancel, - missingAssistant, - thrownError, - returnedError, - ]) - expect(group).toHaveLength(1); - expect(interruptResume).toHaveLength(2); - expect(concurrent).toHaveLength(2); - const cases = { - wait: wait[0], - resume: interruptResume[1], - values: values[0], - messages: messages[0], - updates: updates[0], - left: concurrent.find((event) => - JSON.stringify(event.input).includes("exactly: left"), - )!, - right: concurrent.find((event) => - JSON.stringify(event.input).includes("exactly: right"), - )!, - }; - for (const [name, event] of Object.entries(cases)) { - const usage = expectedUsage[name]; - expect(event.metrics).toMatchObject({ - prompt_tokens: usage.input_tokens, - completion_tokens: usage.output_tokens, - tokens: usage.total_tokens, + const usageLine = result.stdout + .split("\n") + .find((line) => + line.startsWith("LANGGRAPH_EXPECTED_USAGE "), + )!; + const expectedUsage = JSON.parse( + usageLine.slice("LANGGRAPH_EXPECTED_USAGE ".length), + ); + const rawEvents = harness.events(); + const events = [ + ...new Set(rawEvents.map((event) => event.span.name)), + ].flatMap((name) => + name ? findAllSpans(rawEvents, name) : [], + ); + const instrumented = events.filter((event) => + event.span.name?.startsWith("langgraph.runs."), + ); + const root = events.find( + (event) => + event.metadata?.scenario === + "langgraph-sdk-instrumentation", + )!; + expect(root.metadata).toMatchObject({ + sdk_version: version, + module, + instrumentation_mode: mode, + expected_error_cases: 4, }); - expect(event.output).toEqual({ - role: "assistant", - content: - name === "left" || name === "right" - ? name - : "hello from langgraph", + expect(root.output).toEqual({ + status: "passed", + expected_error_cases: 4, }); - } - for (const name of ["values", "messages", "updates"] as const) + expect(events.every((event) => event.span.ended)).toBe(true); + const operationSpecs = [ + [ + "background", + "Uninstrumented background APIs", + "background-apis", + ], + ["wait", "Wait for final result", "wait"], + [ + "interruptResume", + "Interrupt and resume", + "interrupt-resume", + ], + ["values", "Stream values mode", "values"], + ["messages", "Stream messages mode", "messages"], + ["updates", "Stream updates mode", "updates"], + ["streamError", "Stream error", "stream-error"], + ["cancel", "Cancel stream", "cancel"], + [ + "missingAssistant", + "Missing assistant error", + "missing-assistant-error", + ], + ["thrownError", "Thrown graph error", "thrown-error"], + ["returnedError", "Returned graph error", "returned-error"], + ["concurrent", "Concurrent waits", "concurrent-waits"], + ] as const; + const operations = Object.fromEntries( + operationSpecs.map(([key, spanName, operation]) => { + const event = findLatestSpan(rawEvents, spanName)!; + expect(event.span.parentIds).toEqual([root.span.id]); + expect(event.metadata).toMatchObject({ operation }); + return [key, event]; + }), + ) as Record< + (typeof operationSpecs)[number][0], + (typeof events)[number] + >; expect( - cases[name].metrics?.time_to_first_token, - ).toBeGreaterThanOrEqual(0); - for (const name of ["values", "messages", "updates"] as const) - expect(cases[name].output).toEqual(cases.wait.output); - expect(interruptResume[0].output).toBeUndefined(); - expect(interruptResume[0].metadata).toMatchObject({ - "langgraph.interrupts": [ - { value: "Approve the model call?" }, - ], - }); - expect(interruptResume[1].input).toEqual({ - command: { resume: "yes" }, - }); - expect(missingAssistant[0].row.error).toContain("HTTP 404"); - expect(thrownError[0].row.error).toBe("Agent failed"); - expect(returnedError[0].output).toBeUndefined(); - expect(returnedError[0].row.error).toBe("Agent failed"); - const serialized = JSON.stringify(instrumented); - for (const field of [ - "DO_NOT_CAPTURE", - "additional_kwargs", - "response_metadata", - "usage_metadata", - "invalid_tool_calls", - "tool_call_chunks", - ]) - expect(serialized).not.toContain(field); - await matchSpanTreeSnapshot( - events.map((event) => ({ - event, - fields: { - ...spanTreeFields(event), - context: event.context, - }, - })), - resolveFileSnapshotPath( - import.meta.url, - `${dependency}-${module}-${mode}.span-tree.json`, - ), - ); - }, - { - // Keep every mode's assertions, but publish one representative - // trace per version so CI links do not contain duplicate/empty runs. - forwardToProduction: module === "esm" && mode === "wrapped", - }, - ); - }, 120_000); + events.filter((event) => + event.span.parentIds.includes( + operations.background.span.id!, + ), + ), + ).toEqual([]); + if (mode === "disabled") { + expect(instrumented).toHaveLength(0); + expect(events).toHaveLength(13); + return; + } + expect(events).toHaveLength(26); + expect(instrumented).toHaveLength(13); + expect( + instrumented.filter((event) => event.row.error), + ).toHaveLength(4); + for (const event of instrumented) { + expect(event.span.type).toBe("task"); + expect(event.context?.span_origin).toMatchObject({ + instrumentation: { name: "langgraph-sdk" }, + }); + expect(event.span.parentIds).toHaveLength(1); + } + const wait = findChildSpans( + rawEvents, + "langgraph.runs.wait", + operations.wait.span.id, + ); + const interruptResume = findChildSpans( + rawEvents, + "langgraph.runs.wait", + operations.interruptResume.span.id, + ); + const values = findChildSpans( + rawEvents, + "langgraph.runs.stream", + operations.values.span.id, + ); + const messages = findChildSpans( + rawEvents, + "langgraph.runs.stream", + operations.messages.span.id, + ); + const updates = findChildSpans( + rawEvents, + "langgraph.runs.stream", + operations.updates.span.id, + ); + const streamError = findChildSpans( + rawEvents, + "langgraph.runs.stream", + operations.streamError.span.id, + ); + const cancel = findChildSpans( + rawEvents, + "langgraph.runs.stream", + operations.cancel.span.id, + ); + const missingAssistant = findChildSpans( + rawEvents, + "langgraph.runs.wait", + operations.missingAssistant.span.id, + ); + const thrownError = findChildSpans( + rawEvents, + "langgraph.runs.wait", + operations.thrownError.span.id, + ); + const returnedError = findChildSpans( + rawEvents, + "langgraph.runs.wait", + operations.returnedError.span.id, + ); + const concurrent = findChildSpans( + rawEvents, + "langgraph.runs.wait", + operations.concurrent.span.id, + ); + for (const group of [ + wait, + values, + messages, + updates, + streamError, + cancel, + missingAssistant, + thrownError, + returnedError, + ]) + expect(group).toHaveLength(1); + expect(interruptResume).toHaveLength(2); + expect(concurrent).toHaveLength(2); + const cases = { + wait: wait[0], + resume: interruptResume[1], + values: values[0], + messages: messages[0], + updates: updates[0], + left: concurrent.find((event) => + JSON.stringify(event.input).includes("exactly: left"), + )!, + right: concurrent.find((event) => + JSON.stringify(event.input).includes("exactly: right"), + )!, + }; + for (const [name, event] of Object.entries(cases)) { + const usage = expectedUsage[name]; + expect(event.metrics).toMatchObject({ + prompt_tokens: usage.input_tokens, + completion_tokens: usage.output_tokens, + tokens: usage.total_tokens, + }); + expect(event.output).toEqual({ + role: "assistant", + content: + name === "left" || name === "right" + ? name + : "hello from langgraph", + }); + } + for (const name of ["values", "messages", "updates"] as const) + expect( + cases[name].metrics?.time_to_first_token, + ).toBeGreaterThanOrEqual(0); + for (const name of ["values", "messages", "updates"] as const) + expect(cases[name].output).toEqual(cases.wait.output); + expect(interruptResume[0].output).toBeUndefined(); + expect(interruptResume[0].metadata).toMatchObject({ + "langgraph.interrupts": [ + { value: "Approve the model call?" }, + ], + }); + expect(interruptResume[1].input).toEqual({ + command: { resume: "yes" }, + }); + expect(missingAssistant[0].row.error).toContain("HTTP 404"); + expect(thrownError[0].row.error).toBe("Agent failed"); + expect(returnedError[0].output).toBeUndefined(); + expect(returnedError[0].row.error).toBe("Agent failed"); + const serialized = JSON.stringify(instrumented); + for (const field of [ + "DO_NOT_CAPTURE", + "additional_kwargs", + "response_metadata", + "usage_metadata", + "invalid_tool_calls", + "tool_call_chunks", + ]) + expect(serialized).not.toContain(field); + await matchSpanTreeSnapshot( + events.map((event) => ({ + event, + fields: { + ...spanTreeFields(event), + context: event.context, + }, + })), + resolveFileSnapshotPath( + import.meta.url, + `${dependency}-${module}-${mode}.span-tree.json`, + ), + ); + }, + { + // Keep every mode's assertions, but publish one representative + // trace per version so CI links do not contain duplicate/empty runs. + forwardToProduction: module === "esm" && mode === "wrapped", + }, + ); + }, 120_000); + } } - } - }); + }, + ); } }); diff --git a/e2e/scenarios/langsmith-instrumentation/assertions.ts b/e2e/scenarios/langsmith-instrumentation/assertions.ts index ffce28155..6a5c1dbfe 100644 --- a/e2e/scenarios/langsmith-instrumentation/assertions.ts +++ b/e2e/scenarios/langsmith-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { withScenarioHarness } from "../../helpers/scenario-harness"; diff --git a/e2e/scenarios/langsmith-instrumentation/scenario.test.ts b/e2e/scenarios/langsmith-instrumentation/scenario.test.ts index 7aa136d7b..e6094dcda 100644 --- a/e2e/scenarios/langsmith-instrumentation/scenario.test.ts +++ b/e2e/scenarios/langsmith-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -33,7 +33,7 @@ const baseEnv = { describe.concurrent("variants", () => { for (const scenario of scenarios) { - describe.sequential(`langsmith sdk ${scenario.version}`, () => { + describe(`langsmith sdk ${scenario.version}`, { concurrent: false }, () => { defineLangSmithInstrumentationAssertions({ name: "wrapped instrumentation", runScenario: async ({ runScenarioDir }) => { diff --git a/e2e/scenarios/mastra-instrumentation/scenario.test.ts b/e2e/scenarios/mastra-instrumentation/scenario.test.ts index bd65452bc..028a429dd 100644 --- a/e2e/scenarios/mastra-instrumentation/scenario.test.ts +++ b/e2e/scenarios/mastra-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, resolveFileSnapshotPath, diff --git a/e2e/scenarios/mistral-instrumentation/assertions.ts b/e2e/scenarios/mistral-instrumentation/assertions.ts index 116d2b821..6ad9509ed 100644 --- a/e2e/scenarios/mistral-instrumentation/assertions.ts +++ b/e2e/scenarios/mistral-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent, CapturedLogRow, diff --git a/e2e/scenarios/mistral-instrumentation/scenario.test.ts b/e2e/scenarios/mistral-instrumentation/scenario.test.ts index 6bd3264a2..81ae1c652 100644 --- a/e2e/scenarios/mistral-instrumentation/scenario.test.ts +++ b/e2e/scenarios/mistral-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -27,7 +27,7 @@ const mistralScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of mistralScenarios) { - describe.sequential(`mistral sdk ${scenario.version}`, () => { + describe(`mistral sdk ${scenario.version}`, { concurrent: false }, () => { defineMistralInstrumentationAssertions({ name: "wrapped instrumentation", runScenario: async ({ runScenarioDir }) => { diff --git a/e2e/scenarios/nextjs-auto-instrumentation/scenario.test.ts b/e2e/scenarios/nextjs-auto-instrumentation/scenario.test.ts index 42046ad2e..7f614d6dd 100644 --- a/e2e/scenarios/nextjs-auto-instrumentation/scenario.test.ts +++ b/e2e/scenarios/nextjs-auto-instrumentation/scenario.test.ts @@ -1,6 +1,6 @@ import { promises as fs } from "node:fs"; import path from "node:path"; -import { describe, test } from "vitest"; +import { describe, test } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, diff --git a/e2e/scenarios/nextjs-auto-instrumentation/template/app/api/test/route.ts b/e2e/scenarios/nextjs-auto-instrumentation/template/app/api/test/route.ts index 282e699dd..66c3af088 100644 --- a/e2e/scenarios/nextjs-auto-instrumentation/template/app/api/test/route.ts +++ b/e2e/scenarios/nextjs-auto-instrumentation/template/app/api/test/route.ts @@ -1,4 +1,4 @@ -import "braintrust"; // Registers Braintrust's global instrumentation hooks. +import "braintrust/auto"; // Registers Braintrust's global instrumentation hooks. import OpenAI from "openai"; import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; diff --git a/e2e/scenarios/nextjs-instrumentation/scenario.test.ts b/e2e/scenarios/nextjs-instrumentation/scenario.test.ts index 193719a14..e4c5c7ec1 100644 --- a/e2e/scenarios/nextjs-instrumentation/scenario.test.ts +++ b/e2e/scenarios/nextjs-instrumentation/scenario.test.ts @@ -1,6 +1,6 @@ import { promises as fs } from "node:fs"; import path from "node:path"; -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, matchFileSnapshot, diff --git a/e2e/scenarios/ollama-instrumentation/assertions.ts b/e2e/scenarios/ollama-instrumentation/assertions.ts index a8dcd1df7..3d29b0947 100644 --- a/e2e/scenarios/ollama-instrumentation/assertions.ts +++ b/e2e/scenarios/ollama-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/ollama-instrumentation/scenario.test.ts b/e2e/scenarios/ollama-instrumentation/scenario.test.ts index 525d0f504..a1e2c4ca3 100644 --- a/e2e/scenarios/ollama-instrumentation/scenario.test.ts +++ b/e2e/scenarios/ollama-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -30,9 +30,9 @@ const ollamaScenarios = await Promise.all( })), ); -describe.sequential("variants", () => { +describe("variants", { concurrent: false }, () => { for (const scenario of ollamaScenarios) { - describe.sequential(`ollama sdk ${scenario.version}`, () => { + describe(`ollama sdk ${scenario.version}`, { concurrent: false }, () => { defineOllamaInstrumentationAssertions({ name: "wrapped instrumentation", runScenario: async ({ runScenarioDir }) => { diff --git a/e2e/scenarios/openai-agents-api-instrumentation/assertions.ts b/e2e/scenarios/openai-agents-api-instrumentation/assertions.ts index 2e21bbd38..ba53378a6 100644 --- a/e2e/scenarios/openai-agents-api-instrumentation/assertions.ts +++ b/e2e/scenarios/openai-agents-api-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { diff --git a/e2e/scenarios/openai-agents-api-instrumentation/scenario.test.ts b/e2e/scenarios/openai-agents-api-instrumentation/scenario.test.ts index 732bf7df8..288d4ac2b 100644 --- a/e2e/scenarios/openai-agents-api-instrumentation/scenario.test.ts +++ b/e2e/scenarios/openai-agents-api-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -32,8 +32,9 @@ const scenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of scenarios) { - describe.sequential( + describe( `${scenario.variantKey}: OpenAI Agents API SDK ${scenario.version}`, + { concurrent: false }, () => { defineOpenAIAgentsAPIInstrumentationAssertions({ name: "manual instrumentation", diff --git a/e2e/scenarios/openai-agents-instrumentation/assertions.ts b/e2e/scenarios/openai-agents-instrumentation/assertions.ts index 5aa4ee4f7..c16ae698b 100644 --- a/e2e/scenarios/openai-agents-instrumentation/assertions.ts +++ b/e2e/scenarios/openai-agents-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { diff --git a/e2e/scenarios/openai-agents-instrumentation/scenario.test.ts b/e2e/scenarios/openai-agents-instrumentation/scenario.test.ts index 932132258..8da101763 100644 --- a/e2e/scenarios/openai-agents-instrumentation/scenario.test.ts +++ b/e2e/scenarios/openai-agents-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -32,29 +32,33 @@ const openAIAgentsScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of openAIAgentsScenarios) { - describe.sequential(`openai agents sdk ${scenario.version}`, () => { - defineOpenAIAgentsAutoInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - env: { - NODE_ENV: "development", - OPENAI_AGENTS_PACKAGE_NAME: scenario.dependencyName, - }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.variantKey, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.variantKey, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + describe( + `openai agents sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineOpenAIAgentsAutoInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { + NODE_ENV: "development", + OPENAI_AGENTS_PACKAGE_NAME: scenario.dependencyName, + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.variantKey, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/openai-codex-instrumentation/assertions.ts b/e2e/scenarios/openai-codex-instrumentation/assertions.ts index 5d3060f64..57692e800 100644 --- a/e2e/scenarios/openai-codex-instrumentation/assertions.ts +++ b/e2e/scenarios/openai-codex-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { diff --git a/e2e/scenarios/openai-codex-instrumentation/scenario.test.ts b/e2e/scenarios/openai-codex-instrumentation/scenario.test.ts index ca9d3bc1c..b097448ae 100644 --- a/e2e/scenarios/openai-codex-instrumentation/scenario.test.ts +++ b/e2e/scenarios/openai-codex-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -38,45 +38,49 @@ const openAICodexScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of openAICodexScenarios) { - describe.sequential(`openai codex sdk ${scenario.version}`, () => { - defineOpenAICodexInstrumentationAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: scenario.wrapperEntry, - env: { OPENAI_CODEX_PACKAGE_NAME: scenario.dependencyName }, - runContext: { - variantKey: scenario.wrapperSnapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.wrapperSnapshotName, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); + describe( + `openai codex sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineOpenAICodexInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: scenario.wrapperEntry, + env: { OPENAI_CODEX_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + variantKey: scenario.wrapperSnapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.wrapperSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); - defineOpenAICodexInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: scenario.autoEntry, - env: { OPENAI_CODEX_PACKAGE_NAME: scenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.autoSnapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.autoSnapshotName, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + defineOpenAICodexInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: scenario.autoEntry, + env: { OPENAI_CODEX_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.autoSnapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.autoSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/openai-instrumentation/assertions.ts b/e2e/scenarios/openai-instrumentation/assertions.ts index 047a1a581..c5f0f004c 100644 --- a/e2e/scenarios/openai-instrumentation/assertions.ts +++ b/e2e/scenarios/openai-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import type { Json } from "../../helpers/normalize"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; diff --git a/e2e/scenarios/openai-instrumentation/scenario.impl.mjs b/e2e/scenarios/openai-instrumentation/scenario.impl.mjs index 013559d7a..52f762a63 100644 --- a/e2e/scenarios/openai-instrumentation/scenario.impl.mjs +++ b/e2e/scenarios/openai-instrumentation/scenario.impl.mjs @@ -327,7 +327,7 @@ async function runOpenAIMediaOperations(client, openAIMajorVersion) { throw new Error("Expected a WAV speech response"); } let pcm; - for (let offset = 12; offset + 8 <= wav.length; ) { + for (let offset = 12; offset + 8 <= wav.length;) { const length = wav.readUInt32LE(offset + 4); if (wav.toString("ascii", offset, offset + 4) === "data") { pcm = wav.subarray(offset + 8); diff --git a/e2e/scenarios/openai-instrumentation/scenario.test.ts b/e2e/scenarios/openai-instrumentation/scenario.test.ts index dc1fa0e69..b5bbdc767 100644 --- a/e2e/scenarios/openai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/openai-instrumentation/scenario.test.ts @@ -1,5 +1,5 @@ import { defineAttachmentCaptureTests } from "../../helpers/attachment-capture-assertions"; -import { describe, it } from "vitest"; +import { describe, it } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -86,7 +86,7 @@ describe.concurrent("variants", () => { const assertPrivateFieldMethodsOperation = !scenario.disablePrivateFieldMethodsAssertion; - describe.sequential(`openai sdk ${scenario.version}`, () => { + describe(`openai sdk ${scenario.version}`, { concurrent: false }, () => { defineOpenAIInstrumentationAssertions({ assertPrivateFieldMethodsOperation, name: "wrapped instrumentation", diff --git a/e2e/scenarios/openrouter-agent-instrumentation/assertions.ts b/e2e/scenarios/openrouter-agent-instrumentation/assertions.ts index 9decac7b8..0e82930af 100644 --- a/e2e/scenarios/openrouter-agent-instrumentation/assertions.ts +++ b/e2e/scenarios/openrouter-agent-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/openrouter-agent-instrumentation/scenario.test.ts b/e2e/scenarios/openrouter-agent-instrumentation/scenario.test.ts index 58a39ac92..860b904bb 100644 --- a/e2e/scenarios/openrouter-agent-instrumentation/scenario.test.ts +++ b/e2e/scenarios/openrouter-agent-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -32,45 +32,49 @@ const openRouterAgentScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of openRouterAgentScenarios) { - describe.sequential(`openrouter agent ${scenario.version}`, () => { - defineOpenRouterAgentTraceAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: "scenario.ts", - env: { OPENROUTER_AGENT_PACKAGE_NAME: scenario.dependencyName }, - runContext: { - variantKey: scenario.variantKey, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.variantKey}-wrapped`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); + describe( + `openrouter agent ${scenario.version}`, + { concurrent: false }, + () => { + defineOpenRouterAgentTraceAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { OPENROUTER_AGENT_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + variantKey: scenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.variantKey}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); - defineOpenRouterAgentTraceAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: "scenario.mjs", - env: { OPENROUTER_AGENT_PACKAGE_NAME: scenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.variantKey, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: `${scenario.variantKey}-auto`, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + defineOpenRouterAgentTraceAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { OPENROUTER_AGENT_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.variantKey}-auto`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/openrouter-instrumentation/assertions.ts b/e2e/scenarios/openrouter-instrumentation/assertions.ts index 230497dd2..262fa21bd 100644 --- a/e2e/scenarios/openrouter-instrumentation/assertions.ts +++ b/e2e/scenarios/openrouter-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/openrouter-instrumentation/scenario.test.ts b/e2e/scenarios/openrouter-instrumentation/scenario.test.ts index 4f36fd6ad..48cfb0567 100644 --- a/e2e/scenarios/openrouter-instrumentation/scenario.test.ts +++ b/e2e/scenarios/openrouter-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -52,47 +52,51 @@ const openRouterScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of openRouterScenarios) { - describe.sequential(`openrouter sdk ${scenario.version}`, () => { - defineOpenRouterTraceAssertions({ - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: scenario.wrapperEntry, - env: { OPENROUTER_PACKAGE_NAME: scenario.dependencyName }, - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.snapshotName, - supportsRerank: scenario.supportsRerank, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); + describe( + `openrouter sdk ${scenario.version}`, + { concurrent: false }, + () => { + defineOpenRouterTraceAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: scenario.wrapperEntry, + env: { OPENROUTER_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.snapshotName, + supportsRerank: scenario.supportsRerank, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); - defineOpenRouterTraceAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: scenario.autoEntry, - env: { OPENROUTER_PACKAGE_NAME: scenario.dependencyName }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.snapshotName, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.snapshotName, - supportsRerank: scenario.supportsRerank, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + defineOpenRouterTraceAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: scenario.autoEntry, + env: { OPENROUTER_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.snapshotName, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.snapshotName, + supportsRerank: scenario.supportsRerank, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/otel-compat-mixed-tracing/scenario.test.ts b/e2e/scenarios/otel-compat-mixed-tracing/scenario.test.ts index bd91dc47b..9aa4033e7 100644 --- a/e2e/scenarios/otel-compat-mixed-tracing/scenario.test.ts +++ b/e2e/scenarios/otel-compat-mixed-tracing/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { prepareScenarioDir, diff --git a/e2e/scenarios/otel-span-processor-export/scenario.test.ts b/e2e/scenarios/otel-span-processor-export/scenario.test.ts index 54714f6ad..983805534 100644 --- a/e2e/scenarios/otel-span-processor-export/scenario.test.ts +++ b/e2e/scenarios/otel-span-processor-export/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { prepareScenarioDir, resolveScenarioDir, diff --git a/e2e/scenarios/otel-v1-span-processor-export/scenario.test.ts b/e2e/scenarios/otel-v1-span-processor-export/scenario.test.ts index 62e2aadf3..5993e3731 100644 --- a/e2e/scenarios/otel-v1-span-processor-export/scenario.test.ts +++ b/e2e/scenarios/otel-v1-span-processor-export/scenario.test.ts @@ -1,6 +1,6 @@ import { readFile } from "node:fs/promises"; import { createRequire } from "node:module"; -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, diff --git a/e2e/scenarios/pi-coding-agent-instrumentation/assertions.ts b/e2e/scenarios/pi-coding-agent-instrumentation/assertions.ts index 1b13c19a7..8d7b0674e 100644 --- a/e2e/scenarios/pi-coding-agent-instrumentation/assertions.ts +++ b/e2e/scenarios/pi-coding-agent-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/pi-coding-agent-instrumentation/scenario.test.ts b/e2e/scenarios/pi-coding-agent-instrumentation/scenario.test.ts index 6b6575e1c..fcc1f7b6a 100644 --- a/e2e/scenarios/pi-coding-agent-instrumentation/scenario.test.ts +++ b/e2e/scenarios/pi-coding-agent-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -59,8 +59,9 @@ const piCodingAgentScenarios = await Promise.all( describe.concurrent("variants", () => { for (const piCodingAgentScenario of piCodingAgentScenarios) { - describe.sequential( + describe( `pi coding agent ${piCodingAgentScenario.version}`, + { concurrent: false }, () => { definePiCodingAgentInstrumentationAssertions({ name: "wrapped instrumentation", diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/assertions.ts b/e2e/scenarios/strands-agent-sdk-instrumentation/assertions.ts index aae60c0b0..9232bba54 100644 --- a/e2e/scenarios/strands-agent-sdk-instrumentation/assertions.ts +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import type { Json } from "../../helpers/normalize"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts index cbcfd1167..95962813a 100644 --- a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -42,52 +42,56 @@ const strandsAgentSDKScenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of strandsAgentSDKScenarios) { - describe.sequential(`Strands Agent SDK ${scenario.version}`, () => { - defineStrandsAgentSDKInstrumentationAssertions({ - expectOverlapParentProbe: scenario.expectOverlapParentProbe, - name: "wrapped instrumentation", - runScenario: async ({ runScenarioDir }) => { - await runScenarioDir({ - entry: scenario.wrapperEntry, - env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", - STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName, - }, - runContext: { - variantKey: scenario.variantKey, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.wrapperSnapshotName, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); + describe( + `Strands Agent SDK ${scenario.version}`, + { concurrent: false }, + () => { + defineStrandsAgentSDKInstrumentationAssertions({ + expectOverlapParentProbe: scenario.expectOverlapParentProbe, + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: scenario.wrapperEntry, + env: { + STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName, + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + }, + runContext: { + variantKey: scenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.wrapperSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); - defineStrandsAgentSDKInstrumentationAssertions({ - name: "auto-hook instrumentation", - runScenario: async ({ runNodeScenarioDir }) => { - await runNodeScenarioDir({ - entry: scenario.autoEntry, - env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", - STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName, - }, - nodeArgs: ["--import", "braintrust/hook.mjs"], - runContext: { - variantKey: scenario.variantKey, - originalScenarioDir, - }, - scenarioDir, - timeoutMs: TIMEOUT_MS, - }); - }, - snapshotName: scenario.autoSnapshotName, - testFileUrl: import.meta.url, - timeoutMs: TIMEOUT_MS, - }); - }); + defineStrandsAgentSDKInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: scenario.autoEntry, + env: { + STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName, + BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: scenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: scenario.autoSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }, + ); } }); diff --git a/e2e/scenarios/test-framework-evals-jest/__snapshots__/jest-v29-latest.request-flow.json b/e2e/scenarios/test-framework-evals-jest/__snapshots__/jest-v29-latest.request-flow.json index 7d866f4f9..d5da80413 100644 --- a/e2e/scenarios/test-framework-evals-jest/__snapshots__/jest-v29-latest.request-flow.json +++ b/e2e/scenarios/test-framework-evals-jest/__snapshots__/jest-v29-latest.request-flow.json @@ -28,7 +28,7 @@ "rows": [ { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -59,7 +59,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -98,7 +98,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -133,7 +133,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -170,7 +170,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -197,7 +197,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -226,7 +226,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -258,7 +258,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -296,7 +296,7 @@ "rows": [ { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -327,7 +327,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -366,7 +366,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -401,7 +401,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -438,7 +438,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -465,7 +465,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -494,7 +494,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -526,7 +526,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, diff --git a/e2e/scenarios/test-framework-evals-jest/__snapshots__/jest-v29.request-flow.json b/e2e/scenarios/test-framework-evals-jest/__snapshots__/jest-v29.request-flow.json index 7d866f4f9..d5da80413 100644 --- a/e2e/scenarios/test-framework-evals-jest/__snapshots__/jest-v29.request-flow.json +++ b/e2e/scenarios/test-framework-evals-jest/__snapshots__/jest-v29.request-flow.json @@ -28,7 +28,7 @@ "rows": [ { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -59,7 +59,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -98,7 +98,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -133,7 +133,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -170,7 +170,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -197,7 +197,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -226,7 +226,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -258,7 +258,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -296,7 +296,7 @@ "rows": [ { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -327,7 +327,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -366,7 +366,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -401,7 +401,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -438,7 +438,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, @@ -465,7 +465,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -494,7 +494,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "traced.name", "caller_lineno": 0 }, @@ -526,7 +526,7 @@ }, { "context": { - "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.cjs", + "caller_filename": "/e2e/scenarios/test-framework-evals-jest/runner.case.mjs", "caller_functionname": "Object.", "caller_lineno": 0 }, diff --git a/e2e/scenarios/test-framework-evals-jest/jest.config.cjs b/e2e/scenarios/test-framework-evals-jest/jest.config.cjs index 76108d7e0..9205c0f8a 100644 --- a/e2e/scenarios/test-framework-evals-jest/jest.config.cjs +++ b/e2e/scenarios/test-framework-evals-jest/jest.config.cjs @@ -1,5 +1,6 @@ module.exports = { rootDir: __dirname, testEnvironment: "node", - testMatch: ["/runner.case.cjs"], + transform: {}, + testMatch: ["/runner.case.mjs"], }; diff --git a/e2e/scenarios/test-framework-evals-jest/runner.case.cjs b/e2e/scenarios/test-framework-evals-jest/runner.case.mjs similarity index 97% rename from e2e/scenarios/test-framework-evals-jest/runner.case.cjs rename to e2e/scenarios/test-framework-evals-jest/runner.case.mjs index b2886017b..24bde97c6 100644 --- a/e2e/scenarios/test-framework-evals-jest/runner.case.cjs +++ b/e2e/scenarios/test-framework-evals-jest/runner.case.mjs @@ -1,4 +1,6 @@ -const { +import * as braintrust from "braintrust"; +import { test, expect, afterAll } from "@jest/globals"; +import { Eval, JSONAttachment, Prompt, @@ -6,7 +8,7 @@ const { initLogger, startSpan, traced, -} = require("braintrust"); +} from "braintrust"; const testRunId = process.env.BRAINTRUST_E2E_RUN_ID; const scenario = "test-framework-evals-jest"; @@ -32,9 +34,7 @@ function expectNamedExports(module, exportNames) { } } -test("jest exposes the core braintrust runtime surface in CommonJS", () => { - const braintrust = require("braintrust"); - +test("jest exposes the core braintrust runtime surface in ESM", () => { expectNamedExports(braintrust, [ "initLogger", "currentSpan", diff --git a/e2e/scenarios/test-framework-evals-jest/scenario.mjs b/e2e/scenarios/test-framework-evals-jest/scenario.mjs index b923325c5..a692f2a23 100644 --- a/e2e/scenarios/test-framework-evals-jest/scenario.mjs +++ b/e2e/scenarios/test-framework-evals-jest/scenario.mjs @@ -16,7 +16,13 @@ async function runJest() { await new Promise((resolve, reject) => { const child = spawn( process.execPath, - [jestCliPath, "--config", "jest.config.cjs", "--runInBand"], + [ + "--experimental-vm-modules", + jestCliPath, + "--config", + "jest.config.cjs", + "--runInBand", + ], { cwd: scenarioDir, env: process.env, diff --git a/e2e/scenarios/test-framework-evals-jest/scenario.test.ts b/e2e/scenarios/test-framework-evals-jest/scenario.test.ts index f19ca7555..02b0e6f43 100644 --- a/e2e/scenarios/test-framework-evals-jest/scenario.test.ts +++ b/e2e/scenarios/test-framework-evals-jest/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, matchFileSnapshot, diff --git a/e2e/scenarios/test-framework-evals-node/scenario.test.ts b/e2e/scenarios/test-framework-evals-node/scenario.test.ts index 61f1ff244..432f4123d 100644 --- a/e2e/scenarios/test-framework-evals-node/scenario.test.ts +++ b/e2e/scenarios/test-framework-evals-node/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { prepareScenarioDir, diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2-latest.span-tree.json b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2-latest.span-tree.json index fa83fb077..1a37cab1d 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2-latest.span-tree.json +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2-latest.span-tree.json @@ -90,7 +90,7 @@ "scenario": "test-framework-evals-vitest", "testRunId": "" }, - "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at Module.runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at Module.withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at Module.runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" } ] } diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2-latest.span-tree.txt b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2-latest.span-tree.txt index 509669bb4..eaa603b87 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2-latest.span-tree.txt +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2-latest.span-tree.txt @@ -70,4 +70,4 @@ span_tree: "scenario": "test-framework-evals-vitest", "testRunId": "" } - error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at Module.runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at Module.withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at Module.runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2.span-tree.json b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2.span-tree.json index fa83fb077..1a37cab1d 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2.span-tree.json +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2.span-tree.json @@ -90,7 +90,7 @@ "scenario": "test-framework-evals-vitest", "testRunId": "" }, - "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at Module.runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at Module.withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at Module.runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" } ] } diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2.span-tree.txt b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2.span-tree.txt index 509669bb4..eaa603b87 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2.span-tree.txt +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v2.span-tree.txt @@ -70,4 +70,4 @@ span_tree: "scenario": "test-framework-evals-vitest", "testRunId": "" } - error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at Module.runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at Module.withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at Module.runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3-latest.span-tree.json b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3-latest.span-tree.json index 23e801424..fde5790b2 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3-latest.span-tree.json +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3-latest.span-tree.json @@ -90,7 +90,7 @@ "scenario": "test-framework-evals-vitest", "testRunId": "" }, - "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" } ] } diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3-latest.span-tree.txt b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3-latest.span-tree.txt index 6ab1bf413..0a45596ab 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3-latest.span-tree.txt +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3-latest.span-tree.txt @@ -70,4 +70,4 @@ span_tree: "scenario": "test-framework-evals-vitest", "testRunId": "" } - error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3.span-tree.json b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3.span-tree.json index 23e801424..fde5790b2 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3.span-tree.json +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3.span-tree.json @@ -90,7 +90,7 @@ "scenario": "test-framework-evals-vitest", "testRunId": "" }, - "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" } ] } diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3.span-tree.txt b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3.span-tree.txt index 6ab1bf413..0a45596ab 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3.span-tree.txt +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v3.span-tree.txt @@ -70,4 +70,4 @@ span_tree: "scenario": "test-framework-evals-vitest", "testRunId": "" } - error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4-latest.span-tree.json b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4-latest.span-tree.json index 23e801424..fde5790b2 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4-latest.span-tree.json +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4-latest.span-tree.json @@ -90,7 +90,7 @@ "scenario": "test-framework-evals-vitest", "testRunId": "" }, - "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" } ] } diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4-latest.span-tree.txt b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4-latest.span-tree.txt index 6ab1bf413..0a45596ab 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4-latest.span-tree.txt +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4-latest.span-tree.txt @@ -70,4 +70,4 @@ span_tree: "scenario": "test-framework-evals-vitest", "testRunId": "" } - error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4.span-tree.json b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4.span-tree.json index 23e801424..fde5790b2 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4.span-tree.json +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4.span-tree.json @@ -90,7 +90,7 @@ "scenario": "test-framework-evals-vitest", "testRunId": "" }, - "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" } ] } diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4.span-tree.txt b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4.span-tree.txt index 6ab1bf413..0a45596ab 100644 --- a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4.span-tree.txt +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v4.span-tree.txt @@ -70,4 +70,4 @@ span_tree: "scenario": "test-framework-evals-vitest", "testRunId": "" } - error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger.ts:0:0)\n at withCurrent (/js/src/logger.ts:0:0)\n at /js/src/logger.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger.ts:0:0)" + error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5-latest.span-tree.json b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5-latest.span-tree.json new file mode 100644 index 000000000..fde5790b2 --- /dev/null +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5-latest.span-tree.json @@ -0,0 +1,96 @@ +{ + "span_tree": [ + { + "name": "vitest simple pass", + "type": "task", + "children": [], + "output": { + "phase": "simple-pass" + }, + "scores": { + "pass": 1 + }, + "metadata": { + "case": "simple-pass", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + }, + { + "name": "vitest configured span", + "type": "task", + "children": [], + "input": { + "value": 5 + }, + "output": { + "phase": "configured-span", + "result": 10 + }, + "expected": 10, + "scores": { + "correctness": 1, + "pass": 1, + "quality": 0.9 + }, + "tags": [ + "math", + "configured" + ], + "metadata": { + "case": "configured-span", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + }, + { + "name": "vitest concurrent alpha", + "type": "task", + "children": [], + "output": { + "phase": "concurrent-alpha" + }, + "scores": { + "pass": 1 + }, + "metadata": { + "case": "concurrent-alpha", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + }, + { + "name": "vitest concurrent beta", + "type": "task", + "children": [], + "output": { + "phase": "concurrent-beta" + }, + "scores": { + "pass": 1 + }, + "metadata": { + "case": "concurrent-beta", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + }, + { + "name": "vitest expected failure", + "type": "task", + "children": [], + "output": { + "phase": "expected-failure" + }, + "scores": { + "pass": 0 + }, + "metadata": { + "case": "expected-failure", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + }, + "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" + } + ] +} diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5-latest.span-tree.txt b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5-latest.span-tree.txt new file mode 100644 index 000000000..0a45596ab --- /dev/null +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5-latest.span-tree.txt @@ -0,0 +1,73 @@ +span_tree: +├── vitest simple pass [task] +│ output: { +│ "phase": "simple-pass" +│ } +│ scores: { +│ "pass": 1 +│ } +│ metadata: { +│ "case": "simple-pass", +│ "scenario": "test-framework-evals-vitest", +│ "testRunId": "" +│ } +├── vitest configured span [task] +│ input: { +│ "value": 5 +│ } +│ output: { +│ "phase": "configured-span", +│ "result": 10 +│ } +│ expected: 10 +│ scores: { +│ "correctness": 1, +│ "pass": 1, +│ "quality": 0.9 +│ } +│ tags: [ +│ "math", +│ "configured" +│ ] +│ metadata: { +│ "case": "configured-span", +│ "scenario": "test-framework-evals-vitest", +│ "testRunId": "" +│ } +├── vitest concurrent alpha [task] +│ output: { +│ "phase": "concurrent-alpha" +│ } +│ scores: { +│ "pass": 1 +│ } +│ metadata: { +│ "case": "concurrent-alpha", +│ "scenario": "test-framework-evals-vitest", +│ "testRunId": "" +│ } +├── vitest concurrent beta [task] +│ output: { +│ "phase": "concurrent-beta" +│ } +│ scores: { +│ "pass": 1 +│ } +│ metadata: { +│ "case": "concurrent-beta", +│ "scenario": "test-framework-evals-vitest", +│ "testRunId": "" +│ } +└── vitest expected failure [task] + output: { + "phase": "expected-failure" + } + scores: { + "pass": 0 + } + metadata: { + "case": "expected-failure", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5.span-tree.json b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5.span-tree.json new file mode 100644 index 000000000..fde5790b2 --- /dev/null +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5.span-tree.json @@ -0,0 +1,96 @@ +{ + "span_tree": [ + { + "name": "vitest simple pass", + "type": "task", + "children": [], + "output": { + "phase": "simple-pass" + }, + "scores": { + "pass": 1 + }, + "metadata": { + "case": "simple-pass", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + }, + { + "name": "vitest configured span", + "type": "task", + "children": [], + "input": { + "value": 5 + }, + "output": { + "phase": "configured-span", + "result": 10 + }, + "expected": 10, + "scores": { + "correctness": 1, + "pass": 1, + "quality": 0.9 + }, + "tags": [ + "math", + "configured" + ], + "metadata": { + "case": "configured-span", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + }, + { + "name": "vitest concurrent alpha", + "type": "task", + "children": [], + "output": { + "phase": "concurrent-alpha" + }, + "scores": { + "pass": 1 + }, + "metadata": { + "case": "concurrent-alpha", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + }, + { + "name": "vitest concurrent beta", + "type": "task", + "children": [], + "output": { + "phase": "concurrent-beta" + }, + "scores": { + "pass": 1 + }, + "metadata": { + "case": "concurrent-beta", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + }, + { + "name": "vitest expected failure", + "type": "task", + "children": [], + "output": { + "phase": "expected-failure" + }, + "scores": { + "pass": 0 + }, + "metadata": { + "case": "expected-failure", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + }, + "error": "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" + } + ] +} diff --git a/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5.span-tree.txt b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5.span-tree.txt new file mode 100644 index 000000000..0a45596ab --- /dev/null +++ b/e2e/scenarios/test-framework-evals-vitest/__snapshots__/vitest-v5.span-tree.txt @@ -0,0 +1,73 @@ +span_tree: +├── vitest simple pass [task] +│ output: { +│ "phase": "simple-pass" +│ } +│ scores: { +│ "pass": 1 +│ } +│ metadata: { +│ "case": "simple-pass", +│ "scenario": "test-framework-evals-vitest", +│ "testRunId": "" +│ } +├── vitest configured span [task] +│ input: { +│ "value": 5 +│ } +│ output: { +│ "phase": "configured-span", +│ "result": 10 +│ } +│ expected: 10 +│ scores: { +│ "correctness": 1, +│ "pass": 1, +│ "quality": 0.9 +│ } +│ tags: [ +│ "math", +│ "configured" +│ ] +│ metadata: { +│ "case": "configured-span", +│ "scenario": "test-framework-evals-vitest", +│ "testRunId": "" +│ } +├── vitest concurrent alpha [task] +│ output: { +│ "phase": "concurrent-alpha" +│ } +│ scores: { +│ "pass": 1 +│ } +│ metadata: { +│ "case": "concurrent-alpha", +│ "scenario": "test-framework-evals-vitest", +│ "testRunId": "" +│ } +├── vitest concurrent beta [task] +│ output: { +│ "phase": "concurrent-beta" +│ } +│ scores: { +│ "pass": 1 +│ } +│ metadata: { +│ "case": "concurrent-beta", +│ "scenario": "test-framework-evals-vitest", +│ "testRunId": "" +│ } +└── vitest expected failure [task] + output: { + "phase": "expected-failure" + } + scores: { + "pass": 0 + } + metadata: { + "case": "expected-failure", + "scenario": "test-framework-evals-vitest", + "testRunId": "" + } + error: "expected 'wrong' to be 'right' // Object.is equality\n\nAssertionError: expected 'wrong' to be 'right' // Object.is equality\n at /e2e/scenarios/test-framework-evals-vitest/runner.case.ts:0:0\n at Object.fn (/js/src/wrappers/vitest/wrapper.ts:0:0)\n at args.experiment.traced.name (/js/src/wrappers/shared/traced-eval.ts:0:0)\n at /js/src/logger/core.ts:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at /js/src/logger/experiments.ts:0:0\n at runCatchFinally (/js/src/util.ts:0:0)\n at Experiment.traced (/js/src/logger/experiments.ts:0:0)" diff --git a/e2e/scenarios/test-framework-evals-vitest/package.json b/e2e/scenarios/test-framework-evals-vitest/package.json index cd182d5d8..21bb1a649 100644 --- a/e2e/scenarios/test-framework-evals-vitest/package.json +++ b/e2e/scenarios/test-framework-evals-vitest/package.json @@ -15,6 +15,10 @@ "vitest-v4-latest": { "package": "vitest", "range": "4" + }, + "vitest-v5-latest": { + "package": "vitest", + "range": "5" } } } @@ -28,7 +32,9 @@ "vitest-v3": "npm:vitest@3.2.4", "vitest-v3-latest": "npm:vitest@3.2.6", "vitest-v4": "npm:vitest@4.1.5", - "vitest-v4-latest": "npm:vitest@4.1.9" + "vitest-v4-latest": "npm:vitest@4.1.9", + "vitest-v5": "npm:vitest@5.0.3", + "vitest-v5-latest": "npm:vitest@5.0.3" }, "pnpm": { "overrides": { diff --git a/e2e/scenarios/test-framework-evals-vitest/pnpm-lock.yaml b/e2e/scenarios/test-framework-evals-vitest/pnpm-lock.yaml index d4f30efb0..e4db4ce76 100644 --- a/e2e/scenarios/test-framework-evals-vitest/pnpm-lock.yaml +++ b/e2e/scenarios/test-framework-evals-vitest/pnpm-lock.yaml @@ -39,6 +39,12 @@ importers: vitest-v4-latest: specifier: npm:vitest@4.1.9 version: vitest@4.1.9(vite@7.3.1) + vitest-v5: + specifier: npm:vitest@5.0.3 + version: vitest@5.0.3(vite@7.3.1) + vitest-v5-latest: + specifier: npm:vitest@5.0.3 + version: vitest@5.0.3(vite@7.3.1) packages: @@ -336,9 +342,19 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -579,6 +595,17 @@ packages: vite: optional: true + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} @@ -654,6 +681,9 @@ packages: '@vitest/spy@4.1.9': resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} + '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} @@ -761,6 +791,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -831,6 +864,10 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -1173,11 +1210,57 @@ packages: jsdom: optional: true + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -1330,8 +1413,17 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -1524,6 +1616,15 @@ snapshots: optionalDependencies: vite: 7.3.1 + '@vitest/mocker@5.0.3(vite@7.3.1)': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + vite: 7.3.1 + '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 @@ -1641,6 +1742,8 @@ snapshots: '@vitest/spy@4.1.9': {} + '@vitest/spy@5.0.3': {} + '@vitest/utils@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 @@ -1789,6 +1892,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@1.4.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + ms@2.1.3: {} nanoid@3.3.18: {} @@ -1864,6 +1971,8 @@ snapshots: tinybench@2.9.0: {} + tinybench@6.2.2: {} + tinyexec@0.3.2: {} tinyexec@1.0.4: {} @@ -2145,9 +2254,30 @@ snapshots: transitivePeerDependencies: - msw + vitest@5.0.3(vite@7.3.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(vite@7.3.1) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 7.3.1 + why-is-node-running: 3.2.1 + transitivePeerDependencies: + - msw + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + why-is-node-running@3.2.1: {} + zod@4.4.3: {} diff --git a/e2e/scenarios/test-framework-evals-vitest/runner.case.ts b/e2e/scenarios/test-framework-evals-vitest/runner.case.ts index 8e75a95c9..7cac7f359 100644 --- a/e2e/scenarios/test-framework-evals-vitest/runner.case.ts +++ b/e2e/scenarios/test-framework-evals-vitest/runner.case.ts @@ -1,4 +1,3 @@ -import * as path from "node:path"; import { afterAll as vitestAfterAll, beforeAll as vitestBeforeAll, @@ -8,20 +7,7 @@ import { } from "vitest"; import { getTestRunId, scopedName } from "../../helpers/scenario-runtime"; -const repoRoot = process.env.BRAINTRUST_E2E_REPO_ROOT; -if (!repoRoot) { - throw new Error("BRAINTRUST_E2E_REPO_ROOT is not set"); -} - -const { login } = await import(path.join(repoRoot, "js/src/logger.ts")); -const { configureNode } = await import( - path.join(repoRoot, "js/src/node/config.ts") -); -const { wrapVitest } = await import( - path.join(repoRoot, "js/src/wrappers/vitest/index.ts") -); - -configureNode(); +import { login, wrapVitest } from "braintrust"; const testRunId = getTestRunId(); const scenario = "test-framework-evals-vitest"; diff --git a/e2e/scenarios/test-framework-evals-vitest/scenario.test.ts b/e2e/scenarios/test-framework-evals-vitest/scenario.test.ts index 75968b70e..94e42ce08 100644 --- a/e2e/scenarios/test-framework-evals-vitest/scenario.test.ts +++ b/e2e/scenarios/test-framework-evals-vitest/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { prepareScenarioDir, @@ -21,6 +21,12 @@ interface VitestScenario { } const scenarios: VitestScenario[] = [ + ...["vitest-v5", "vitest-v5-latest"].map((name) => ({ + dependencyName: name, + entry: "scenario.vitest-v5.ts", + label: name, + variantKey: name, + })), { dependencyName: "vitest-v2", entry: "scenario.ts", diff --git a/e2e/scenarios/test-framework-evals-vitest/scenario.vitest-v5.ts b/e2e/scenarios/test-framework-evals-vitest/scenario.vitest-v5.ts new file mode 100644 index 000000000..f4a9092df --- /dev/null +++ b/e2e/scenarios/test-framework-evals-vitest/scenario.vitest-v5.ts @@ -0,0 +1,48 @@ +import { createRequire } from "node:module"; +import { promises as fs } from "node:fs"; +import * as path from "node:path"; +import { resolveScenarioDir } from "../../helpers/scenario-harness"; +import { + getTestRunId, + runMain, + runNodeSubprocess, +} from "../../helpers/scenario-runtime"; + +const require = createRequire(import.meta.url); +const scenarioDir = resolveScenarioDir(import.meta.url); + +// Resolve the vitest.mjs bin by finding the package root via the main entry. +async function findVitestBin(packageName: string): Promise { + const entryPath = require.resolve(packageName); + let dir = path.dirname(entryPath); + while (dir !== path.dirname(dir)) { + const candidate = path.join(dir, "vitest.mjs"); + try { + await fs.access(candidate); + return candidate; + } catch { + // Keep walking upward. + } + dir = path.dirname(dir); + } + throw new Error(`Could not find vitest.mjs for ${packageName}`); +} + +async function main() { + const vitestPackageName = + process.env.VITEST_PACKAGE_NAME ?? "vitest-v5-latest"; + const vitestCliPath = await findVitestBin(vitestPackageName); + const testRunId = getTestRunId(); + + await runNodeSubprocess({ + args: [vitestCliPath, "run", "--config", "vitest.runner-v5.config.mts"], + cwd: scenarioDir, + env: { + BRAINTRUST_E2E_RUN_ID: testRunId, + VITEST_CONFIG_PACKAGE_NAME: vitestPackageName, + }, + timeoutMs: 60_000, + }); +} + +runMain(main); diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts index f2a1afe65..4b33922a2 100644 --- a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-evals-reporter.config.mts @@ -1,20 +1,11 @@ -import * as path from "node:path"; -import { pathToFileURL } from "node:url"; import { defineConfig } from "vitest/config"; -const repoRoot = process.env.BRAINTRUST_E2E_REPO_ROOT; -if (!repoRoot) { - throw new Error("BRAINTRUST_E2E_REPO_ROOT is not set"); -} - const testRunId = process.env.BRAINTRUST_E2E_RUN_ID; if (!testRunId) { throw new Error("BRAINTRUST_E2E_RUN_ID is not set"); } -const { default: BraintrustVitestEvalsReporter } = await import( - pathToFileURL(path.join(repoRoot, "js/dist/vitest-evals-reporter.mjs")).href -); +import BraintrustVitestEvalsReporter from "braintrust/vitest-evals-reporter"; export default defineConfig({ test: { diff --git a/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v5.config.mts b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v5.config.mts new file mode 100644 index 000000000..4e000c5a8 --- /dev/null +++ b/e2e/scenarios/test-framework-evals-vitest/vitest.runner-v5.config.mts @@ -0,0 +1,13 @@ +const vitestConfigPackageName = + process.env.VITEST_CONFIG_PACKAGE_NAME ?? "vitest-v5-latest"; +const { defineConfig } = await import(`${vitestConfigPackageName}/config`); + +export default defineConfig({ + test: { + clearMocks: false, + sharedViteServer: false, + hookTimeout: 30_000, + include: ["runner.case.ts"], + testTimeout: 20_000, + }, +}); diff --git a/e2e/scenarios/trace-context-and-continuation/scenario.test.ts b/e2e/scenarios/trace-context-and-continuation/scenario.test.ts index f386798e2..49dafc0b9 100644 --- a/e2e/scenarios/trace-context-and-continuation/scenario.test.ts +++ b/e2e/scenarios/trace-context-and-continuation/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, matchFileSnapshot, diff --git a/e2e/scenarios/trace-primitives-basic/__snapshots__/request-flow.json b/e2e/scenarios/trace-primitives-basic/__snapshots__/request-flow.json index bcc576917..45d1a039f 100644 --- a/e2e/scenarios/trace-primitives-basic/__snapshots__/request-flow.json +++ b/e2e/scenarios/trace-primitives-basic/__snapshots__/request-flow.json @@ -109,7 +109,7 @@ "caller_lineno": 0 }, "created": "", - "error": "basic boom\n\nError: basic boom\n at logger.traced.name (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at /js/dist/index.mjs:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.mjs:0:0)\n at withCurrent (/js/dist/index.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at runCatchFinally (/js/dist/index.mjs:0:0)\n at Logger.traced (/js/dist/index.mjs:0:0)\n at main (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at process.processTicksAndRejections (node::0:0)", + "error": "basic boom\n\nError: basic boom\n at logger.traced.name (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at (/js/src/logger/core.ts:0:0)\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at (/js/src/logger/core.ts:0:0)\n at runCatchFinally (/js/src/util.ts:0:0)\n at Logger.traced (/js/src/logger/core.ts:0:0)\n at main (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at process.processTicksAndRejections (node::0:0)", "id": "", "log_id": "g", "metadata": { @@ -214,7 +214,7 @@ "caller_lineno": 0 }, "created": "", - "error": "basic boom\n\nError: basic boom\n at logger.traced.name (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at /js/dist/index.mjs:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.mjs:0:0)\n at withCurrent (/js/dist/index.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at runCatchFinally (/js/dist/index.mjs:0:0)\n at Logger.traced (/js/dist/index.mjs:0:0)\n at main (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at process.processTicksAndRejections (node::0:0)", + "error": "basic boom\n\nError: basic boom\n at logger.traced.name (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at (/js/src/logger/core.ts:0:0)\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at (/js/src/logger/core.ts:0:0)\n at runCatchFinally (/js/src/util.ts:0:0)\n at Logger.traced (/js/src/logger/core.ts:0:0)\n at main (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at process.processTicksAndRejections (node::0:0)", "id": "", "log_id": "g", "metadata": { diff --git a/e2e/scenarios/trace-primitives-basic/__snapshots__/span-tree.json b/e2e/scenarios/trace-primitives-basic/__snapshots__/span-tree.json index 852eddd49..2dd9ee811 100644 --- a/e2e/scenarios/trace-primitives-basic/__snapshots__/span-tree.json +++ b/e2e/scenarios/trace-primitives-basic/__snapshots__/span-tree.json @@ -26,7 +26,7 @@ "kind": "basic-error", "testRunId": "" }, - "error": "basic boom\n\nError: basic boom\n at logger.traced.name (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at /js/dist/index.mjs:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.mjs:0:0)\n at withCurrent (/js/dist/index.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at runCatchFinally (/js/dist/index.mjs:0:0)\n at Logger.traced (/js/dist/index.mjs:0:0)\n at main (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at process.processTicksAndRejections (node::0:0)" + "error": "basic boom\n\nError: basic boom\n at logger.traced.name (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at (/js/src/logger/core.ts:0:0)\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at (/js/src/logger/core.ts:0:0)\n at runCatchFinally (/js/src/util.ts:0:0)\n at Logger.traced (/js/src/logger/core.ts:0:0)\n at main (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at process.processTicksAndRejections (node::0:0)" } ], "input": { diff --git a/e2e/scenarios/trace-primitives-basic/__snapshots__/span-tree.txt b/e2e/scenarios/trace-primitives-basic/__snapshots__/span-tree.txt index dd6da9af4..296f115b4 100644 --- a/e2e/scenarios/trace-primitives-basic/__snapshots__/span-tree.txt +++ b/e2e/scenarios/trace-primitives-basic/__snapshots__/span-tree.txt @@ -28,4 +28,4 @@ span_tree: "kind": "basic-error", "testRunId": "" } - error: "basic boom\n\nError: basic boom\n at logger.traced.name (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at /js/dist/index.mjs:0:0\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/dist/index.mjs:0:0)\n at withCurrent (/js/dist/index.mjs:0:0)\n at /js/dist/index.mjs:0:0\n at runCatchFinally (/js/dist/index.mjs:0:0)\n at Logger.traced (/js/dist/index.mjs:0:0)\n at main (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at process.processTicksAndRejections (node::0:0)" + error: "basic boom\n\nError: basic boom\n at logger.traced.name (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at (/js/src/logger/core.ts:0:0)\n at AsyncLocalStorage.run (node::0:0)\n at BraintrustContextManager.runInContext (/js/src/logger/core.ts:0:0)\n at withCurrent (/js/src/logger/core.ts:0:0)\n at (/js/src/logger/core.ts:0:0)\n at runCatchFinally (/js/src/util.ts:0:0)\n at Logger.traced (/js/src/logger/core.ts:0:0)\n at main (/e2e/scenarios/trace-primitives-basic/scenario.ts:0:0)\n at process.processTicksAndRejections (node::0:0)" diff --git a/e2e/scenarios/trace-primitives-basic/scenario.test.ts b/e2e/scenarios/trace-primitives-basic/scenario.test.ts index ac13928d2..7a8d89d6e 100644 --- a/e2e/scenarios/trace-primitives-basic/scenario.test.ts +++ b/e2e/scenarios/trace-primitives-basic/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { formatJsonFileSnapshot, matchFileSnapshot, diff --git a/e2e/scenarios/typesafe-instrumentation/assertions.ts b/e2e/scenarios/typesafe-instrumentation/assertions.ts index f49d686ce..a2dd446e2 100644 --- a/e2e/scenarios/typesafe-instrumentation/assertions.ts +++ b/e2e/scenarios/typesafe-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/typesafe-instrumentation/scenario.test.ts b/e2e/scenarios/typesafe-instrumentation/scenario.test.ts index 838b56b1e..fb70e83e6 100644 --- a/e2e/scenarios/typesafe-instrumentation/scenario.test.ts +++ b/e2e/scenarios/typesafe-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -36,7 +36,7 @@ const scenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of scenarios) { - describe.sequential(`typesafe sdk ${scenario.version}`, () => { + describe(`typesafe sdk ${scenario.version}`, { concurrent: false }, () => { const env = { TYPESAFE_PACKAGE_NAME: scenario.dependencyName }; const runContext = { originalScenarioDir, diff --git a/e2e/scenarios/voyageai-instrumentation/assertions.ts b/e2e/scenarios/voyageai-instrumentation/assertions.ts index 9927d23d7..cd6ead8d9 100644 --- a/e2e/scenarios/voyageai-instrumentation/assertions.ts +++ b/e2e/scenarios/voyageai-instrumentation/assertions.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, expect, test } from "vite-plus/test"; import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; import { diff --git a/e2e/scenarios/voyageai-instrumentation/scenario.test.ts b/e2e/scenarios/voyageai-instrumentation/scenario.test.ts index 6431662a7..72b483d72 100644 --- a/e2e/scenarios/voyageai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/voyageai-instrumentation/scenario.test.ts @@ -1,4 +1,4 @@ -import { describe } from "vitest"; +import { describe } from "vite-plus/test"; import { prepareScenarioDir, readInstalledPackageVersion, @@ -36,7 +36,7 @@ const scenarios = await Promise.all( describe.concurrent("variants", () => { for (const scenario of scenarios) { - describe.sequential(`voyageai sdk ${scenario.version}`, () => { + describe(`voyageai sdk ${scenario.version}`, { concurrent: false }, () => { const env = { VOYAGEAI_PACKAGE_NAME: scenario.dependencyName, }; diff --git a/e2e/scenarios/workflow-eval-batch/scenario.test.ts b/e2e/scenarios/workflow-eval-batch/scenario.test.ts index 0062d4a16..7c21ede1c 100644 --- a/e2e/scenarios/workflow-eval-batch/scenario.test.ts +++ b/e2e/scenarios/workflow-eval-batch/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { prepareScenarioDir, resolveScenarioDir, diff --git a/e2e/scenarios/workflow-eval-webhook/scenario.test.ts b/e2e/scenarios/workflow-eval-webhook/scenario.test.ts index a81e7785c..66c8a448d 100644 --- a/e2e/scenarios/workflow-eval-webhook/scenario.test.ts +++ b/e2e/scenarios/workflow-eval-webhook/scenario.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { prepareScenarioDir, resolveScenarioDir, diff --git a/e2e/scripts/build-pr-e2e-links-comment.mjs b/e2e/scripts/build-pr-e2e-links-comment.mjs index 7c2aaaf41..7518526b0 100644 --- a/e2e/scripts/build-pr-e2e-links-comment.mjs +++ b/e2e/scripts/build-pr-e2e-links-comment.mjs @@ -57,12 +57,18 @@ function btqlFilterClause(predicate) { }; } -function buildSearchParam(metadataScenario, testRunIds) { +function buildSearchParam(metadataScenario, testRunIds, rootSpanIds) { return JSON.stringify({ - filter: [ - btqlFilterClause(scenarioPredicate(metadataScenario)), - btqlFilterClause(testRunIdPredicate(testRunIds)), - ], + filter: rootSpanIds?.length + ? [ + btqlFilterClause( + `root_span_id IN (${rootSpanIds.map(quoteFilterValue).join(", ")})`, + ), + ] + : [ + btqlFilterClause(scenarioPredicate(metadataScenario)), + btqlFilterClause(testRunIdPredicate(testRunIds)), + ], }); } @@ -230,6 +236,12 @@ async function readRunContextRecords(runContextDir) { : DEFAULT_VARIANT_KEY; records.push({ entry: typeof parsed.entry === "string" ? parsed.entry : null, + logRootSpanIds: Array.isArray(parsed.logRootSpanIds) + ? parsed.logRootSpanIds.filter((id) => typeof id === "string") + : undefined, + experimentNames: Array.isArray(parsed.experimentNames) + ? parsed.experimentNames.filter((name) => typeof name === "string") + : undefined, scenarioDirName, testRunId: parsed.testRunId, variantKey, @@ -414,7 +426,23 @@ function buildCommentBody(options) { return; } - const search = buildSearchParam(metadataScenario, observedRunIds); + const records = options.runContextRecords.filter((record) => + observedRunIds.includes(record.testRunId), + ); + const rootSpanIds = [ + ...new Set(records.flatMap((record) => record.logRootSpanIds ?? [])), + ]; + const experimentNames = [ + ...new Set(records.flatMap((record) => record.experimentNames ?? [])), + ]; + const legacyRecords = records.some( + (record) => record.logRootSpanIds === undefined, + ); + const search = buildSearchParam( + metadataScenario, + observedRunIds, + rootSpanIds, + ); const logsUrl = buildLogsUrl({ appUrl: options.appPublicUrl, orgName: options.orgName, @@ -427,7 +455,21 @@ function buildCommentBody(options) { ? `Observed (${runCount} ${runWord}, ${statusSuffix})` : `Observed (${runCount} ${runWord})`; - lines.push(`| ${rowLabel} | [Open logs](${logsUrl}) | ${status} |`); + const links = [ + ...(rootSpanIds.length || legacyRecords + ? [`[Open logs](${logsUrl})`] + : []), + ...experimentNames.map((experimentName, index) => { + const url = buildExperimentUrl({ + appUrl: options.appPublicUrl, + orgName: options.orgName, + projectName: options.projectName, + experimentName, + }); + return `[Open eval${experimentNames.length > 1 ? ` ${index + 1}` : ""}](${url})`; + }), + ]; + lines.push(`| ${rowLabel} | ${links.join("
") || "N/A"} | ${status} |`); }; for (const scenario of options.scenarios) { diff --git a/e2e/scripts/run-e2e-tests.mjs b/e2e/scripts/run-e2e-tests.mjs index 301ecd83d..14522d1ea 100644 --- a/e2e/scripts/run-e2e-tests.mjs +++ b/e2e/scripts/run-e2e-tests.mjs @@ -12,7 +12,7 @@ const VITEST_COMMAND = path.join( E2E_DIR, "node_modules", ".bin", - process.platform === "win32" ? "vitest.cmd" : "vitest", + process.platform === "win32" ? "vp.cmd" : "vp", ); const DEFAULT_OPENAI_CODEX_E2E_MODEL = "gpt-5.3-codex"; @@ -44,6 +44,7 @@ const testTargets = ? scenarioArgs.map((arg) => scenarioPathArg(arg)) : [...(await defaultScenarioTestPaths()), "helpers"]; const vitestArgs = [ + "test", "run", "--run", ...testTargets, diff --git a/e2e/scripts/run-record-tests.mjs b/e2e/scripts/run-record-tests.mjs index 15f6d536b..78b2bed23 100644 --- a/e2e/scripts/run-record-tests.mjs +++ b/e2e/scripts/run-record-tests.mjs @@ -18,7 +18,7 @@ const VITEST_COMMAND = path.join( E2E_DIR, "node_modules", ".bin", - process.platform === "win32" ? "vitest.cmd" : "vitest", + process.platform === "win32" ? "vp.cmd" : "vp", ); const rawArgs = process.argv.slice(2).filter((arg) => arg !== "--"); @@ -220,7 +220,7 @@ async function runVitest(scenarioNames, runContextDir) { "--update", ]; - return await runProcess(VITEST_COMMAND, vitestArgs, { + return await runProcess(VITEST_COMMAND, ["test", ...vitestArgs], { cwd: E2E_DIR, env, }); diff --git a/e2e/vitest.config.mts b/e2e/vite.config.ts similarity index 66% rename from e2e/vitest.config.mts rename to e2e/vite.config.ts index 0e89f07c3..c225346c6 100644 --- a/e2e/vitest.config.mts +++ b/e2e/vite.config.ts @@ -1,7 +1,13 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vite-plus"; export default defineConfig({ test: { + // Vitest v4 compatibility: retain until the complete Harness Docker matrix + // can validate both changes. Local replay passes without clearMocks, but the + // remote Docker host cannot reach this workspace's cassette server. + // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings + clearMocks: false, + sharedViteServer: false, reporters: ["default"], include: ["scenarios/**/*.test.ts", "helpers/**/*.test.ts"], // We run the scenarios in the hooks. diff --git a/integrations/browser-js/package.json b/integrations/browser-js/package.json index e74dc1c1b..93facc836 100644 --- a/integrations/browser-js/package.json +++ b/integrations/browser-js/package.json @@ -8,7 +8,6 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js", "default": "./dist/index.js" } }, @@ -17,9 +16,9 @@ "README.md" ], "scripts": { - "build": "tsup", + "build": "vp run compile", "check:typings": "tsc --noEmit", - "watch": "tsup --watch", + "watch": "vp pack --watch", "clean": "rm -rf dist", "yalc:publish": "yalc publish" }, @@ -29,8 +28,8 @@ }, "devDependencies": { "@types/node": "^20.10.5", - "tsup": "^8.5.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vite-plus": "catalog:" }, "peerDependencies": { "zod": "^3.25.34 || ^4.0" @@ -51,5 +50,12 @@ "access": "public", "registry": "https://registry.npmjs.org/", "provenance": true - } + }, + "engines": { + "node": ">=22.18.0" + }, + "sideEffects": [ + "./dist/index.js" + ], + "main": "./dist/index.js" } diff --git a/integrations/browser-js/tsup.config.ts b/integrations/browser-js/tsup.config.ts deleted file mode 100644 index b532d4d5c..000000000 --- a/integrations/browser-js/tsup.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - format: ["esm"], - dts: true, - sourcemap: true, - clean: true, - external: ["braintrust", "zod"], - target: "es2022", - platform: "browser", - outDir: "./dist", - treeshake: true, -}); diff --git a/integrations/browser-js/turbo.json b/integrations/browser-js/turbo.json deleted file mode 100644 index 6b90853c5..000000000 --- a/integrations/browser-js/turbo.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"], - "tasks": { - "build": { - "dependsOn": ["braintrust#build"], - "outputs": ["dist/**"] - } - } -} diff --git a/integrations/browser-js/vite.config.ts b/integrations/browser-js/vite.config.ts new file mode 100644 index 000000000..4fced1cad --- /dev/null +++ b/integrations/browser-js/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + run: { + tasks: { + compile: { + command: "vp pack", + dependsOn: [ + { task: "compile", from: ["dependencies", "devDependencies"] }, + ], + cache: { + input: [{ auto: true }, "!dist/**"], + untrackedEnv: ["SHELL", "BASH_ENV"], + output: ["dist/**"], + }, + }, + }, + }, + pack: { + dts: { generator: "oxc" }, + target: "es2022", + platform: "browser", + }, +}); diff --git a/integrations/deepseek-harness/.gitignore b/integrations/deepseek-harness/.gitignore new file mode 100644 index 000000000..7a6c09645 --- /dev/null +++ b/integrations/deepseek-harness/.gitignore @@ -0,0 +1 @@ +.vitest/ diff --git a/integrations/deepseek-harness/package.json b/integrations/deepseek-harness/package.json index cd2fb2aad..84f3775e6 100644 --- a/integrations/deepseek-harness/package.json +++ b/integrations/deepseek-harness/package.json @@ -3,15 +3,13 @@ "version": "1.0.0", "description": "Braintrust tracing plugin for DeepSeek Harness", "main": "./dist/index.js", - "module": "./dist/index.mjs", + "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { "./package.json": "./package.json", ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" + "default": "./dist/index.js" }, "./cordis.patch.yml": "./cordis.patch.yml" }, @@ -21,11 +19,11 @@ "README.md" ], "scripts": { - "build": "tsup", + "build": "vp run compile", "check:typings": "tsc --noEmit", - "watch": "tsup --watch", + "watch": "vp pack --watch", "clean": "rm -r dist/*", - "test": "vitest run", + "test": "vp test run", "yalc:publish": "yalc publish" }, "dsh": { @@ -65,9 +63,8 @@ "@deepseek-ai/dsh-session": "0.1.0-rc.6", "@deepseek-ai/dsh-tools": "0.1.0-rc.6", "@types/node": "^22.15.21", - "tsup": "^8.5.1", "typescript": "^5.5.4", - "vitest": "4.1.11" + "vite-plus": "catalog:" }, "repository": { "type": "git", @@ -79,5 +76,10 @@ "access": "public", "registry": "https://registry.npmjs.org/", "provenance": true - } + }, + "type": "module", + "engines": { + "node": ">=22.18.0" + }, + "sideEffects": false } diff --git a/integrations/deepseek-harness/src/index.test.ts b/integrations/deepseek-harness/src/index.test.ts index ca318eee3..6e3e03bf5 100644 --- a/integrations/deepseek-harness/src/index.test.ts +++ b/integrations/deepseek-harness/src/index.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vite-plus/test"; const mock = vi.hoisted(() => { let nextId = 0; diff --git a/integrations/deepseek-harness/src/index.ts b/integrations/deepseek-harness/src/index.ts index 4ce891edd..128feb88f 100644 --- a/integrations/deepseek-harness/src/index.ts +++ b/integrations/deepseek-harness/src/index.ts @@ -37,7 +37,7 @@ const INSTRUMENTATION_SYMBOL = Symbol.for("braintrust.spanInstrumentationName"); export const name = "braintrust"; /** Harness services whose event seams this plugin instruments. */ -export const inject = ["sessions", "llm", "tools"]; +export const inject: string[] = ["sessions", "llm", "tools"]; export interface Config { /** Braintrust API key. Falls back to BRAINTRUST_API_KEY when omitted. */ diff --git a/integrations/deepseek-harness/tsconfig.json b/integrations/deepseek-harness/tsconfig.json index ea6ad1861..72b024bb4 100644 --- a/integrations/deepseek-harness/tsconfig.json +++ b/integrations/deepseek-harness/tsconfig.json @@ -2,14 +2,14 @@ "compilerOptions": { "declaration": true, "lib": ["es2022"], - "module": "commonjs", + "module": "esnext", "target": "es2022", - "moduleResolution": "node", + "moduleResolution": "bundler", "strict": true, "noUnusedLocals": true, "esModuleInterop": true, "skipLibCheck": true }, - "include": ["."], + "include": ["src"], "exclude": ["node_modules/**", "**/dist/**"] } diff --git a/integrations/deepseek-harness/tsup.config.ts b/integrations/deepseek-harness/tsup.config.ts deleted file mode 100644 index 7d69621aa..000000000 --- a/integrations/deepseek-harness/tsup.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig([ - { - entry: ["src/index.ts"], - format: ["cjs", "esm"], - outDir: "dist", - dts: true, - }, -]); diff --git a/integrations/deepseek-harness/vite.config.ts b/integrations/deepseek-harness/vite.config.ts new file mode 100644 index 000000000..aa7f69ca2 --- /dev/null +++ b/integrations/deepseek-harness/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + run: { + tasks: { + compile: { + command: "vp pack", + dependsOn: [ + { task: "compile", from: ["dependencies", "devDependencies"] }, + ], + cache: { + input: [{ auto: true }, "!dist/**"], + untrackedEnv: ["SHELL", "BASH_ENV"], + output: ["dist/**"], + }, + }, + }, + }, + pack: { + dts: { generator: "oxc" }, + sourcemap: true, + fixedExtension: false, + }, +}); diff --git a/integrations/otel-js/.gitignore b/integrations/otel-js/.gitignore new file mode 100644 index 000000000..7a6c09645 --- /dev/null +++ b/integrations/otel-js/.gitignore @@ -0,0 +1 @@ +.vitest/ diff --git a/integrations/otel-js/CLAUDE.md b/integrations/otel-js/CLAUDE.md index 5c83827fd..a11b4ca9f 100644 --- a/integrations/otel-js/CLAUDE.md +++ b/integrations/otel-js/CLAUDE.md @@ -20,13 +20,13 @@ sdk/integrations/otel-js/ ├── otel-v1/ # Test environment for OTel 1.x │ ├── src -> ../src # SYMLINK to parent src/ │ ├── package.json # Pins OTel 1.x versions -│ ├── vitest.config.ts # Aliases to local node_modules +│ ├── vite.config.ts # Aliases to local node_modules │ ├── pnpm-lock.yaml # Independent lockfile │ └── node_modules/ # Isolated v1.x dependencies └── otel-v2/ # Test environment for OTel 2.x ├── src -> ../src # SYMLINK to parent src/ ├── package.json # Pins OTel 2.x versions - ├── vitest.config.ts # Aliases to local node_modules + ├── vite.config.ts # Aliases to local node_modules ├── pnpm-lock.yaml # Independent lockfile └── node_modules/ # Isolated v2.x dependencies ``` @@ -162,10 +162,10 @@ This is rarely needed but useful for troubleshooting or regenerating dependencie ### 5. Vitest Configuration -Each test environment has a `vitest.config.ts` that creates module aliases: +Each test environment has a `vite.config.ts` that creates module aliases: ```typescript -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vite-plus"; import { detectOtelVersion, logOtelVersions, diff --git a/integrations/otel-js/Dockerfile.test b/integrations/otel-js/Dockerfile.test index 84c76f791..eaee521f6 100644 --- a/integrations/otel-js/Dockerfile.test +++ b/integrations/otel-js/Dockerfile.test @@ -33,17 +33,10 @@ WORKDIR /workspace # Copy the entire SDK repository (build context is SDK repo root) COPY . . -# Build braintrust SDK first (required peer dependency) -WORKDIR /workspace/js -RUN pnpm install --ignore-workspace -RUN pnpm run build - -# Build @braintrust/otel package -WORKDIR /workspace/integrations/otel-js -# Replace workspace:* with file: path for braintrust dependency -RUN sed -i.bak 's/"braintrust": "workspace:\*"/"braintrust": "file:..\/..\/js"/' package.json && rm -f package.json.bak -RUN pnpm install --ignore-workspace -RUN pnpm run build +# Build SDK packages with the workspace catalog and lockfile. The test packages +# still install independently to keep their OpenTelemetry versions isolated. +RUN pnpm install --frozen-lockfile +RUN pnpm exec vp run --filter=braintrust --filter=@braintrust/otel build # Set the test directory as environment variable (available at runtime) ENV TEST_DIR=${TEST_DIR} diff --git a/integrations/otel-js/otel-v1/package.json b/integrations/otel-js/otel-v1/package.json index 3b95e2fbe..a2ee206b9 100644 --- a/integrations/otel-js/otel-v1/package.json +++ b/integrations/otel-js/otel-v1/package.json @@ -3,24 +3,10 @@ "version": "0.0.1-test", "private": true, "description": "Test package for @braintrust/otel with OpenTelemetry 1.x", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, "scripts": { - "build": "tsup", - "watch": "tsup --watch", "clean": "rm -rf dist/*", - "build:deps": "(cd ../../.. && pnpm turbo run build --filter=braintrust --filter=@braintrust/otel)", - "test": "pnpm run build:deps && vitest run" + "build:deps": "(cd ../../.. && pnpm exec vp run --filter=braintrust --filter=@braintrust/otel build)", + "test": "pnpm run build:deps && vp test run" }, "author": "Braintrust Data Inc.", "license": "MIT", @@ -38,9 +24,10 @@ "@types/node": "^22.15.21", "braintrust": "file:../../../js", "msw": "^2.6.6", - "tsup": "^8.5.0", "typescript": "5.5.4", - "vitest": "4.1.11" + "vitest": "5.0.3", + "vite-plus": "1.1.0", + "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0" }, "pnpm": { "overrides": { diff --git a/integrations/otel-js/otel-v1/pnpm-lock.yaml b/integrations/otel-js/otel-v1/pnpm-lock.yaml index 8aa1764a9..7262a9032 100644 --- a/integrations/otel-js/otel-v1/pnpm-lock.yaml +++ b/integrations/otel-js/otel-v1/pnpm-lock.yaml @@ -66,58 +66,39 @@ importers: msw: specifier: ^2.6.6 version: 2.14.6(@types/node@22.19.19)(typescript@5.5.4) - tsup: - specifier: ^8.5.0 - version: 8.5.1(postcss@8.5.28)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 + vite: + specifier: npm:@voidzero-dev/vite-plus-core@1.1.0 + version: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)' + vite-plus: + specifier: 1.1.0 + version: 1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4) vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)) + specifier: 5.0.3 + version: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) packages: - '@braintrust/bt-darwin-arm64@0.12.0': - resolution: {integrity: sha512-mY6VW/3VwcOQOGN8sYHS6F0xzHTFwgZcNlj7zlQttI6OXOCGt/bhonGIqd03QBhxmj0M31ymSS7TqSeCK/RYIQ==} - cpu: [arm64] - os: [darwin] - - '@braintrust/bt-darwin-x64@0.12.0': - resolution: {integrity: sha512-woyRyDv2DfCF8+von+3X9f1cddAbFnKLSfCbEkrBQVc0ZsAM60as8nehYv7DkafJF/67yKFx/sUraV65sukesQ==} - cpu: [x64] - os: [darwin] - - '@braintrust/bt-linux-arm64@0.12.0': - resolution: {integrity: sha512-J9/7f3EIMKmFmSSQHQnAQLpUgB8YJENrOlkABjlTprBgsIYVgz7tSH7yg2P3ur+2Jem7PpGnrDxHGh/UjRfjsg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@braintrust/bt-linux-x64-musl@0.12.0': - resolution: {integrity: sha512-KnLgENOoztBXcH+mLFJe4bYzi67kSOuELOQrm4Hler35GjgaBMI1fFLIUjKRPAmyT9VIhBMhy1ICfGEFLueMEQ==} - cpu: [x64] - os: [linux] - libc: [musl] + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} - '@braintrust/bt-linux-x64@0.12.0': - resolution: {integrity: sha512-HJFbUl3HYYY1Ivw8OYBeIMcIsU9r7+fC9BzLWB5FdH7881ToKee2wbbLZssMCxFbMVeUxzEo+SzGD/1Z9Gk9CA==} - cpu: [x64] - os: [linux] - libc: [glibc] + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} - '@braintrust/bt-win32-arm64@0.12.0': - resolution: {integrity: sha512-+7PkdBAmiqEJsWteGHP/Zs62F75PkHPA8m/ej4TOz/mHGKulUU0bZibw91KRqIB7J7jGi5ItvCiqkiGaLKLnyw==} - cpu: [arm64] - os: [win32] + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} - '@braintrust/bt-win32-x64@0.12.0': - resolution: {integrity: sha512-Q0c+pVUGm82n/7N8QJ5s6j/G/Q0GFzqOaTipHjkmElLbAOOEcfRH/uljdtIPNBiEQcrzqirS0GmOgiFkeQ3Txg==} - cpu: [x64] - os: [win32] + '@blazediff/core@1.10.0': + resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==} '@braintrust/otel@file:..': resolution: {directory: .., type: directory} + engines: {node: '>=22.18.0'} peerDependencies: '@opentelemetry/api': '>=1.9.0' '@opentelemetry/core': '>=1.9.0' @@ -125,325 +106,156 @@ packages: '@opentelemetry/sdk-trace-base': '>=1.9.0' braintrust: '>=1.0.0-0' - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} @@ -522,16 +334,6 @@ packages: resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} - '@napi-rs/wasm-runtime@1.2.4': - resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - - '@next/env@14.2.35': - resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -724,292 +526,345 @@ packages: resolution: {integrity: sha512-po7penSfQ/Z8352lRVDpaBrd9znwA5mHGqXR7nDEiVnxkDFkBIhVf/tKeAJDIq/erFpcRowKFeCsr5eqqcSyFQ==} engines: {node: '>=14'} - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} - - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + '@oxc-project/runtime@0.153.0': + resolution: {integrity: sha512-rXJvtlsVG4xJNq64hiaUTIatE7jbBgpa6l4QgmEa2jLgT3ULH9scUBkpsC9N1DfmqixLEyT6DuK6OSXozziIlw==} + engines: {node: ^20.19.0 || >=22.12.0} - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + '@oxc-project/types@0.153.0': + resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@oxfmt/binding-android-arm-eabi@0.72.0': + resolution: {integrity: sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + '@oxfmt/binding-android-arm64@0.72.0': + resolution: {integrity: sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + '@oxfmt/binding-darwin-arm64@0.72.0': + resolution: {integrity: sha512-LjQ8tlevdtwraFlWC6AnQ1Forzde0clRmnsP3QmK+ydSElDt/J+5Jc+m+//Ly1G97aP7WCNUw110QMIUw2K7xA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + '@oxfmt/binding-darwin-x64@0.72.0': + resolution: {integrity: sha512-+ZbcxhB1C34RQR5IldSyY6Z/yalJIxHXhP36PGE4sEq0f/lgL5jCLmOKoDeVm8dqd9gFGDsd1bNWHSGCAwdSkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + '@oxfmt/binding-freebsd-x64@0.72.0': + resolution: {integrity: sha512-yGPhA8sePEBFV9iGcprhyOk3Bz3fZshswMH3DpYIPZFLT1DG5LjiZ0j6AJQpSueaL6bUK0av1K+5+fe+ldy5EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + '@oxfmt/binding-linux-arm-gnueabihf@0.72.0': + resolution: {integrity: sha512-/g5axuqFMcAtoQrFLR5EJEMDjR5Ea4GuzpdDmMpPWAgrF1DmL4kD1NPNVlpXEOxpFJnD6lPvLKYj1lzdlrfifA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.72.0': + resolution: {integrity: sha512-InVitdHbS09PM+w+WinIiyX37rC9R3L7lUP9mKy9uKTEDSUFaLjWsLoreRIxKivnv7Ndqc6Trvi+K+b6ZzhiJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + '@oxfmt/binding-linux-arm64-gnu@0.72.0': + resolution: {integrity: sha512-WKjlyRAxpPjXoqeuwFBim5LscgvY+RNEiZ971C9JlRQZONhJTsXDki/oHFYQE+fzzRpzXh3owZvOdT3kNL2g+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + '@oxfmt/binding-linux-arm64-musl@0.72.0': + resolution: {integrity: sha512-poafnbsPpJhR+5Mbwpf8CscugTyiRAzjTOxRVn8ra3X2jnURs2xL5Du2xl5g2iW85lICMglpmLf8jDjZEbNgzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + '@oxfmt/binding-linux-ppc64-gnu@0.72.0': + resolution: {integrity: sha512-zNUi0gKRsO7S3IfPzsTbnm88OedPuX32hmZFWZRWHvWJlAoZRwVbeO6Nb3hng4vDgL7qUKp4TGqzPz1mhsycYg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + '@oxfmt/binding-linux-riscv64-gnu@0.72.0': + resolution: {integrity: sha512-qTY7z/iyF/rWI2HWwWMevXqZW5PZUOofUy2afK7v6rseXnpld3yjgyIk6pCux3DqWQ0lB+f5atvLJNBwzLvFsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.72.0': + resolution: {integrity: sha512-w18qfo8vEMD6EKEHtzTvjc6jH8yCyXIT1Bn7PFWJd0PpZyzH/VQDx4xvjCh0LGz6GRzqiezbisTdG9AoXtpkZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.72.0': + resolution: {integrity: sha512-grW8HwEir+vK7fCcq1ZgL3NHQCfZ1PidiERE9Hbf+24xGys+DoDaoqTht4ccfn0PcBwbViLkwkda/KNpJu2KRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + '@oxfmt/binding-linux-x64-gnu@0.72.0': + resolution: {integrity: sha512-wQJRQfWBRIn88Pkr449EUViwRayrYUScg7ty+Ihiem21mQ9BUA6S9e/4RAgyNk9lMag0KBouyKskjq61rOC0WQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + '@oxfmt/binding-linux-x64-musl@0.72.0': + resolution: {integrity: sha512-T1B0uQzafxuGPjjaPrZJ497nxoi1BiI65JfsYc56nNpmev3msfb/iaTmH2kQuNFqa8PKxpMlf3GhWrWBvBPORQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + '@oxfmt/binding-openharmony-arm64@0.72.0': + resolution: {integrity: sha512-OKxxgTurl7+hypVplO8QRAbbQlwEs0j1Y6FZv+ZN2qNbdxQk2muMlzuJNBGTCWwxQ0rkkOD1uK/DW1fvzoUSJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + '@oxfmt/binding-win32-arm64-msvc@0.72.0': + resolution: {integrity: sha512-L78TKzURqvjuZOCo88ixMbPXf/RP7u31Y0cmjJ/KEmrHmfYhmhEvsDaTYPiyTRne64jUgCbADfY0YeoQk4/Xyw==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] + cpu: [arm64] + os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + '@oxfmt/binding-win32-ia32-msvc@0.72.0': + resolution: {integrity: sha512-gQtFW+Ii8BWgtJTAD17TY/WsdNS5gUfAPov8iI1jeq4r9lRRYImmxPL08UU00/RLaCfBJwh2UgINnDY0iOiZ+A==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [ia32] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + '@oxfmt/binding-win32-x64-msvc@0.72.0': + resolution: {integrity: sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@oxlint-tsgolint/darwin-arm64@7.0.2003': + resolution: {integrity: sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@7.0.2003': + resolution: {integrity: sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@7.0.2003': + resolution: {integrity: sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@7.0.2003': + resolution: {integrity: sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@7.0.2003': + resolution: {integrity: sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@7.0.2003': + resolution: {integrity: sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==} + cpu: [x64] + os: [win32] - '@rollup/rollup-android-arm-eabi@4.60.4': - resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + '@oxlint/binding-android-arm-eabi@1.87.0': + resolution: {integrity: sha512-22tVIHlB9898OYsBKmYB6He1MCSqdTmK1BVWuSNUTVrpS0KWNege+U9uEYqGJ+KatQag0r/oU44i3OOs46z7HQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.4': - resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + '@oxlint/binding-android-arm64@1.87.0': + resolution: {integrity: sha512-JfZQ3MWf74n8ov0DojZpkOduIubYJvZL5tbTMMhDutsal6u1xH1QuNpN+lK+EQwF8AeAfgwTR2fgh5U2br27XA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.4': - resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + '@oxlint/binding-darwin-arm64@1.87.0': + resolution: {integrity: sha512-rkx6Xvvw93z2GImVOyvbZwAZcoY0Z6MJW177XNsSB+i/CBsH7ANyelTp01VZyIkM3iSXtqKVb4+ptBgPHap5pQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.4': - resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + '@oxlint/binding-darwin-x64@1.87.0': + resolution: {integrity: sha512-cw5nYw7L/pQ2SgGBgOcg/Qo89llQre3vIc/kuBLhSICzu1r5s8L76H3ug6AIFsD98WpROj8/z5JVzkf4daxHaQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.4': - resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.4': - resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + '@oxlint/binding-freebsd-x64@1.87.0': + resolution: {integrity: sha512-x/SuznE6loRZK9gz9AS4ZmzNzSu+5KdeoCS+sVHhnrE8HM6AD8UyTjwldQTTqlXGvkwHASgB0NiTNOc3o3PdAg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + '@oxlint/binding-linux-arm-gnueabihf@1.87.0': + resolution: {integrity: sha512-+JEDmCAXiiFmKhAjZhgAOwaHWY6WENOQ5QA+lFuY59tgsUCDOTRi46B+lENK6CWvtxsXpwkZXNOgpjAQcpIpnA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + '@oxlint/binding-linux-arm-musleabihf@1.87.0': + resolution: {integrity: sha512-/uC9XqXLAC9AHFLNycwmSQEvKqGrOCgU+uXALI7gNW0DG81TJ/3T2eHnKdRvcb5R1gFEfjxDwI4e4zIa29rICQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.4': - resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + '@oxlint/binding-linux-arm64-gnu@1.87.0': + resolution: {integrity: sha512-FTRYPsBy/M728vnUv7l+KAwSAa3Qi4BQbhVvy7NYuMTZ0sUa/OWsbMx79HVFw6u/5KWcaYayDsoOiv7xbevzDg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.4': - resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + '@oxlint/binding-linux-arm64-musl@1.87.0': + resolution: {integrity: sha512-6sC+N7IY0h1egd8SrYUGNN7CUyKJZ4bIZakKa7V7hfinf4jfi8Kq4+klJvFp5DqXcwMn44I5MeBwmJluO1odFg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.4': - resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.4': - resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + '@oxlint/binding-linux-ppc64-gnu@1.87.0': + resolution: {integrity: sha512-d5Mnn+ICbhoJPi37Ccjy+86T5qfH2+mSTrTMBXNPfB+wqSWOayBBLLIrCXdUy1YssluCVdGkl+DO9w5r5ODmUA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.4': - resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + '@oxlint/binding-linux-riscv64-gnu@1.87.0': + resolution: {integrity: sha512-sf5YWuUo1Dx9loqWCgF8P278MycfcD4JHjDC/LV5j11L3Lqa4PFiwCjD6B8kP4AjsgZZkLn3xbpJEtZBaccs4Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.4': - resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + '@oxlint/binding-linux-riscv64-musl@1.87.0': + resolution: {integrity: sha512-+aCraYbmK3uU3P0rFojjeaK9pGY6JvBDXh/0CM+gPyWKmNmXmnFe0qj4qj02U4ibFpbd9Pa9CmDvIdjJUd29rA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.4': - resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + '@oxlint/binding-linux-s390x-gnu@1.87.0': + resolution: {integrity: sha512-7rzbZQIljo6VLWVPqUyLa2P/WV3VFsJB4M52R4NCUJn1A50yjtUsHxiMHFL9opEKGFIiTbZ3VQzmtcJOPUq4NQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.4': - resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + '@oxlint/binding-linux-x64-gnu@1.87.0': + resolution: {integrity: sha512-Dry46Fa+AXE/xt9J/YriujU1pU9mzfNWHGQwtOYibAFH8HLrKWCi93AhIGnuUMiwn2JbuBkRyz51f50Ly4r4ow==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.4': - resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + '@oxlint/binding-linux-x64-musl@1.87.0': + resolution: {integrity: sha512-f/HMZez74gJvnRpGz+b5sdGyWrEgRLmFTXg9sNoqjPWogGxYHUhXKTKyMiYsu9rUGR2ciYqL3fk+Ua4JhMJbRA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.4': - resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.4': - resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + '@oxlint/binding-openharmony-arm64@1.87.0': + resolution: {integrity: sha512-lEC6ZfPZm/07JlB7FsOWX9o1RiHlXiBW594LSUGuXmekC8LZmvUPP9XzE2WZcNxJi5cHA/Fw56o02c3JxgMA5g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.4': - resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + '@oxlint/binding-win32-arm64-msvc@1.87.0': + resolution: {integrity: sha512-afBWKOD36dUiA9AenaiWpCg8xfyUVPYznyAAVXu413brK4RA5UI/Xoc4yfeGIT8G1a44MV+JR7hioryerBDzGQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.4': - resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + '@oxlint/binding-win32-ia32-msvc@1.87.0': + resolution: {integrity: sha512-daptNsEOuru4/nJTxV4DBHNJ5GepzmaBVQf+jjgKQGi9NRlZKik+RFq4hUL7ewsF1pwjKM+SkYaMbF82UwFGnw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.4': - resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.4': - resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + '@oxlint/binding-win32-x64-msvc@1.87.0': + resolution: {integrity: sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@oxlint/plugins@1.79.0': + resolution: {integrity: sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@tybys/wasm-util@0.10.4': - resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - '@types/node@22.19.19': - resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/user-event@14.6.8': + resolution: {integrity: sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.19.19': + resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} @@ -1026,11 +881,18 @@ packages: '@aws-sdk/credential-provider-web-identity': optional: true - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + '@vitest/browser-preview@5.0.3': + resolution: {integrity: sha512-jfnEBv5yDRPR1MeiTlTW5bcZoPsiI7MBhpZR8IJiSxVhdK6Pa4gPk3tLHTRxVHy24kkWxY2TnUbnszU3J1bQJw==} + peerDependencies: + vitest: 5.0.3 + + '@vitest/browser@5.0.3': + resolution: {integrity: sha512-7KHIkVA/gTRxQV9qPa9qLK+g5INZG6iRrlwXSFG5EKsOomW3UzzW7wsdt1Yp5PufzQ43lniuONlb4lUVCC2qSg==} + peerDependencies: + vitest: 5.0.3 - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} peerDependencies: msw: ^2.4.9 vite: ^8.0.5 @@ -1040,24 +902,302 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + '@vitest/pretty-format@5.0.3': + resolution: {integrity: sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==} + + '@vitest/snapshot@5.0.3': + resolution: {integrity: sha512-SK4YTg9jL+jwakHIZxfciuvyOQsbY15AueR3USGtsXzmy+3FbJgqjAbC+ac20mRLwfEDtO5sfTqK6bUCDTRVPA==} + + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} + + '@vitest/ui@5.0.3': + resolution: {integrity: sha512-9dfvqDBvSbHM6y9YVXxAypkl2z7mrWozK/JFo237qI0cj/3M/kzDKiVZqzi3TE54Ler7Jditud7OMBZCyUvRHA==} + peerDependencies: + vitest: 5.0.3 + + '@vitest/utils@5.0.3': + resolution: {integrity: sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==} - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + '@voidzero-dev/vite-plus-core@1.1.0': + resolution: {integrity: sha512-Ai6HQP1zG0H3ZrmStEaMVKtDzW0xU3VgMzm3LFaXDQMEp8nAqvp0VGoU8HmpB9xv9vDSl+UxiSNOpuY9T714OQ==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@types/node': ^20.19.0 || >=22.12.0 + '@typescript/native-preview': '*' + '@vitejs/devtools': ^0.7.1 + '@volar/typescript': ~2.4.0 + '@vue/language-core': ~3.2.0 || ~3.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + publint: ^0.3.8 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: '>=0.5.0' + unrun: '*' + vue-tsc: ~3.2.0 || ~3.3.0 + yaml: ^2.4.2 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@types/node': + optional: true + '@typescript/native-preview': + optional: true + '@vitejs/devtools': + optional: true + '@volar/typescript': + optional: true + '@vue/language-core': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + vue-tsc: + optional: true + yaml: + optional: true - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + '@voidzero-dev/vite-plus-darwin-arm64@1.1.0': + resolution: {integrity: sha512-iWJdQZBRZWB6X1Aadk0Rrs/NJeP32Qb0OGeWnu1AYL0g6ZCj1yy3l9Oa+HasDyG97s+4UYo523LdBAAv3dlFwg==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [darwin] - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + '@voidzero-dev/vite-plus-darwin-x64@1.1.0': + resolution: {integrity: sha512-IYSQ2KdigRMuVkKCC+e5uNliCTSHa8j3ajHsFG2cfXoiGHS1f4ausbMEzNYtn9Tcn0Xsyo7l0LN8lvB7MapJhA==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-PmrJG8Md5KCWi/akWJz5GRjei2QfRf48R4rPB9CfualzAAlpMOmaMGeV3eloWVAFye85uhdtzCmIAqN5OUVBJA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} + '@voidzero-dev/vite-plus-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-zFr57++m5U474XEMcFGV025TOv9V2I5d87yylcuI/SvxQwDE9P7T5iRl55vGzQOzzInSnQuru6ByNIfmGa+84A==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-DXeRuTMylEbIVCQX+pSBM27lSPQwAKO6PDnGBJ6nFGhu37AmNeVl7cIChtcniXOuykbFduhSt3eIr/6CZHoEag==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-J7/Mb/kxtENUsL5Gh+8SSr/p054fSqU4oj0GEthr/2VEFSwZouOUigk6CzE7ANKFeTNJm40n8QCnjm7Oiy09pQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-EXWz8Tt7skmI0fLMSq5zYcuHo6SCF0reG55qDB/NMvWVIAca6Dborq7D1HyKA8v6p39zOl8r07gR7yXuYv7eeQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [win32] + + '@voidzero-dev/vite-plus-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-K9k77Mj/TszFHKsZePKsyfsIPo8HbjKeJUxaSJ6YZKAmSh9QvDsMFe4i4/HALwyii/PR1EQ8N0iN1nf9cNimGQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@yuku-codegen/binding-android-arm64@0.9.5': + resolution: {integrity: sha512-jrOY5WM+AaAqkv51fHP1x28ifto4WgcZVzodLUyMU1jMWn5Sq+VciRdk/n8E0Ey5w4p4cWWE+m5GfXWOYh7Kzw==} + cpu: [arm64] + os: [android] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-darwin-arm64@0.9.5': + resolution: {integrity: sha512-4O4lkCQIzPZGjiNB1GORSI6hBECbiiSBS/APZXPvNKYH+nhY4uuqv03LNXA+SET3hoBjvr95P5rIhY8KQaQUBA==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-darwin-x64@0.9.5': + resolution: {integrity: sha512-9EvoUO0SEhD6/d8VHdWuPerepPMSR1y84+UEgz8Un1Ope14Oe7xMvePpEQLTLovoIFZ8Zg3iZ8z8E11pZMqC3g==} + cpu: [x64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-freebsd-x64@0.9.5': + resolution: {integrity: sha512-HqT78WwgHTmp8lwujoUa9CrIortX4DdpuiVC18ZPSGvuuJf4ylpIEI6QrQEM78Zwz3muhAWAOXZ5irdiYY+AyA==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': + resolution: {integrity: sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm-musl@0.9.5': + resolution: {integrity: sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-win32-arm64@0.9.5': + resolution: {integrity: sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w==} + cpu: [arm64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-win32-x64@0.9.5': + resolution: {integrity: sha512-FxENahEjWSan59Syh/us/Kf4wNq8qrJLFJ3R2N4Oiwtb6yNdz/rWLNTIoNSssnsp7IoWJdUP6o/Z8ppg7lXMcg==} + cpu: [x64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-parser/binding-android-arm64@0.9.5': + resolution: {integrity: sha512-A2JCFCSHfnficqYEw4Iujpx7XrkMM3UfFcgJFmYpZSo9zM4nyhmdIIE0FogSduuU60lhM0/UcfuUBXIVNBMlGQ==} + cpu: [arm64] + os: [android] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-darwin-arm64@0.9.5': + resolution: {integrity: sha512-3PiyU+Eare4YuKaQ22N98/yAiROPY5o/NQJHraICzDvk4pgS+m+bgLKOvkGBRn33OnV95Vdv1Mn38b+MQHpULQ==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-darwin-x64@0.9.5': + resolution: {integrity: sha512-blFMAFI7AInI83XaiOF8cIeiRM46Nz9EfpZtZPRLbKxSA9aAr5v8aHYpmfN6NoyXxAv/10pwS4gsAuc1W6fy5Q==} + cpu: [x64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-freebsd-x64@0.9.5': + resolution: {integrity: sha512-TsNuL4qsZdO0tHp5GW43Y5fHeLPpPHA675wdcPNTcdq2ZO5AvsQWFFoiDg8CH4oBktfsQ9tdvKhjLnLYwKvp4g==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-gnu@0.9.5': + resolution: {integrity: sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-musl@0.9.5': + resolution: {integrity: sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-win32-arm64@0.9.5': + resolution: {integrity: sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg==} + cpu: [arm64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-win32-x64@0.9.5': + resolution: {integrity: sha512-FrERt9YWatY3bJfSUdi4YWY+6iQcyo3MzCC821BxlWM5TFZEHijnpz/bknR79VHlXY/9CvXz8ZlaAGPsTtN3nw==} + cpu: [x64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-toolchain/types@0.9.5': + resolution: {integrity: sha512-KiuLNNgX9uNealaWAR+G3/cMXnRk9x4TY2EkYe/KIag+UPdwiA0RRf1hr1WAxzTP8KGzCTkqUdLPvqh32sEO3w==} acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} @@ -1083,11 +1223,12 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} @@ -1097,69 +1238,28 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} - engines: {node: '>=18'} - - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} - engines: {node: 20 || >=22} - braintrust@file:../../../js: resolution: {directory: ../../../js, type: directory} + engines: {node: '>=22.18.0'} hasBin: true peerDependencies: + '@types/react': '>=18' zod: ^3.25.34 || ^4.0 + peerDependenciesMeta: + '@types/react': + optional: true bufrw@1.4.0: resolution: {integrity: sha512-sWm8iPbqvL9+5SiYxXH73UOkyEbGQg7kyHQmReF89WJHQJw2eV4P/yZ0E+b71cczJ4pPobVhXxgQcmfSTgGHxQ==} engines: {node: '>= 0.10.x'} - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - cli-progress@3.12.0: - resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} - engines: {node: '>=4'} - - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1175,48 +1275,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1226,39 +1291,27 @@ packages: supports-color: optional: true - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - error@7.0.2: resolution: {integrity: sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw==} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -1266,15 +1319,6 @@ packages: es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} - es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -1284,9 +1328,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -1298,10 +1339,6 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - eventsource-parser@1.1.2: resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} engines: {node: '>=14.18'} @@ -1310,10 +1347,6 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1338,20 +1371,11 @@ packages: picomatch: optional: true - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1365,26 +1389,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graphql@16.14.0: resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - hasown@2.0.3: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} @@ -1397,21 +1405,6 @@ packages: engines: {node: '>= 0.10.x'} hasBin: true - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} @@ -1423,16 +1416,12 @@ packages: is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - jaeger-client@3.19.0: resolution: {integrity: sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw==} engines: {node: '>=10'} - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -1511,17 +1500,6 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -1535,46 +1513,27 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} meriyah@6.1.4: resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} engines: {node: '>=18.0.0'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1596,40 +1555,18 @@ packages: resolution: {integrity: sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.19: resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - obug@2.2.1: resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - opentracing@0.14.7: resolution: {integrity: sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==} engines: {node: '>=0.10'} @@ -1637,9 +1574,35 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} + oxfmt@0.72.0: + resolution: {integrity: sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@7.0.2003: + resolution: {integrity: sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==} + hasBin: true + + oxlint@1.87.0: + resolution: {integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1647,9 +1610,6 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1660,39 +1620,18 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: ^8.5.18 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} postcss@8.5.28: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + process@0.10.1: resolution: {integrity: sha512-dyIett8dgGIZ/TXKUzeYExt7WA6ldDzys9vTDU/cCA9L17Ypme+KzS+NjQCjpn9xsvi/shbMC+yP/BcFMBz0NA==} engines: {node: '>= 0.6.0'} @@ -1701,25 +1640,8 @@ packages: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} - engines: {node: '>=0.6'} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -1733,10 +1655,6 @@ packages: resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==} engines: {node: '>=6'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -1745,23 +1663,6 @@ packages: rettime@0.11.11: resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rollup@4.60.4: - resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semifies@1.0.0: resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} @@ -1770,46 +1671,20 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - set-cookie-parser@3.1.0: resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1818,9 +1693,6 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -1842,11 +1714,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -1855,40 +1722,27 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - termi-link@1.1.0: - resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} - engines: {node: '>=12'} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - thriftrw@3.11.4: resolution: {integrity: sha512-UcuBd3eanB3T10nXWRRMwfwoaC6VMk7qe3/5YIWP2Jtw+EbHqJ0p1/K3x8ixiR5dozKSSfcg1W+0e33G1Di3XA==} engines: {node: '>= 0.10.x'} hasBin: true - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} tinyexec@1.3.1: resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} engines: {node: '>=18'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@2.2.0: + resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.1: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} @@ -1900,66 +1754,26 @@ packages: resolution: {integrity: sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==} hasBin: true - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.5.18 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - type-fest@5.6.0: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} @@ -1971,67 +1785,36 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vite@8.0.14: - resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} - engines: {node: ^20.19.0 || >=22.12.0} + vite-plus@1.1.0: + resolution: {integrity: sha512-khkRlY70qqbBV0QmF9QKXSrhxIRYftb1H05AJMowMoq7iouchmAoXYeReZ8pKKMOadqrWVxFa3S9BA2m7w59iA==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + playwright: '*' peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: + '@vitest/browser-playwright': optional: true - tsx: + '@vitest/browser-webdriverio': optional: true - yaml: + playwright: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 happy-dom: '*' jsdom: '*' vite: ^8.0.5 @@ -2062,17 +1845,26 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} hasBin: true wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + 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 xorshift@1.2.0: resolution: {integrity: sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g==} @@ -2093,6 +1885,15 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yuku-ast@0.9.5: + resolution: {integrity: sha512-Q8qW8WwQnN5Cm0ZZivdRIfv0sRLTjUq0YumXJkw8CYN1aCdICH9rk4C47/4n6kA5EqDtlj+F608twoTSV2MwdQ==} + + yuku-codegen@0.9.5: + resolution: {integrity: sha512-zGUVyDpSK4b6c9B0yyxi2P0wujn1XZTbG9dCb7d6gyAoP63EMgyLzUwPUvwxPG5jgYqhlI7w+S3oCyapqGr4PA==} + + yuku-parser@0.9.5: + resolution: {integrity: sha512-IBnAdNVMswWbJcM7woPluOudectJzFjJ1N8jVRxP9Uq4CIv5hZdBIcdmtRbFDYF/Ph2j2gVup4YJ4N9mh0jYFA==} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -2103,26 +1904,17 @@ packages: snapshots: - '@braintrust/bt-darwin-arm64@0.12.0': - optional: true - - '@braintrust/bt-darwin-x64@0.12.0': - optional: true - - '@braintrust/bt-linux-arm64@0.12.0': - optional: true - - '@braintrust/bt-linux-x64-musl@0.12.0': - optional: true + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 - '@braintrust/bt-linux-x64@0.12.0': - optional: true + '@babel/helper-validator-identifier@7.29.7': {} - '@braintrust/bt-win32-arm64@0.12.0': - optional: true + '@babel/runtime@7.29.10': {} - '@braintrust/bt-win32-x64@0.12.0': - optional: true + '@blazediff/core@1.10.0': {} '@braintrust/otel@file:..(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.9.0(@opentelemetry/api@1.9.0))(@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.9.0(@opentelemetry/api@1.9.0))(braintrust@file:../../../js(zod@4.4.3))': dependencies: @@ -2132,178 +1924,81 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.9.0(@opentelemetry/api@1.9.0) braintrust: file:../../../js(zod@4.4.3) - '@colors/colors@1.5.0': - optional: true - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.27.7': - optional: true - '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.7': - optional: true - '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.7': - optional: true - '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.7': - optional: true - '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.7': - optional: true - '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.7': - optional: true - '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.7': - optional: true - '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.7': - optional: true - '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.7': - optional: true - '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.7': - optional: true - '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.7': - optional: true - '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.7': - optional: true - '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.7': - optional: true - '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.7': - optional: true - '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.7': - optional: true - '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.7': - optional: true - '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.7': - optional: true - '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.7': - optional: true - '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.7': - optional: true - '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': - optional: true - '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.7': - optional: true - '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.7': - optional: true - '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.7': - optional: true - '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.7': - optional: true - '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.7': - optional: true - '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.7': - optional: true - '@esbuild/win32-x64@0.28.1': optional: true @@ -2383,15 +2078,6 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.4 - optional: true - - '@next/env@14.2.35': {} - '@open-draft/deferred-promise@2.2.0': {} '@open-draft/deferred-promise@3.0.0': {} @@ -2619,160 +2305,182 @@ snapshots: '@opentelemetry/semantic-conventions@1.9.0': {} - '@oxc-project/types@0.132.0': {} - - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.1': {} - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 + '@oxc-project/runtime@0.153.0': {} - '@protobufjs/float@1.0.2': {} + '@oxc-project/types@0.153.0': {} - '@protobufjs/path@1.1.2': {} + '@oxfmt/binding-android-arm-eabi@0.72.0': + optional: true - '@protobufjs/pool@1.1.0': {} + '@oxfmt/binding-android-arm64@0.72.0': + optional: true - '@protobufjs/utf8@1.1.1': {} + '@oxfmt/binding-darwin-arm64@0.72.0': + optional: true - '@rolldown/binding-android-arm64@1.0.2': + '@oxfmt/binding-darwin-x64@0.72.0': optional: true - '@rolldown/binding-darwin-arm64@1.0.2': + '@oxfmt/binding-freebsd-x64@0.72.0': optional: true - '@rolldown/binding-darwin-x64@1.0.2': + '@oxfmt/binding-linux-arm-gnueabihf@0.72.0': optional: true - '@rolldown/binding-freebsd-x64@1.0.2': + '@oxfmt/binding-linux-arm-musleabihf@0.72.0': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + '@oxfmt/binding-linux-arm64-gnu@0.72.0': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': + '@oxfmt/binding-linux-arm64-musl@0.72.0': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': + '@oxfmt/binding-linux-ppc64-gnu@0.72.0': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': + '@oxfmt/binding-linux-riscv64-gnu@0.72.0': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': + '@oxfmt/binding-linux-riscv64-musl@0.72.0': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': + '@oxfmt/binding-linux-s390x-gnu@0.72.0': optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': + '@oxfmt/binding-linux-x64-gnu@0.72.0': optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': + '@oxfmt/binding-linux-x64-musl@0.72.0': optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@oxfmt/binding-openharmony-arm64@0.72.0': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': + '@oxfmt/binding-win32-arm64-msvc@0.72.0': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': + '@oxfmt/binding-win32-ia32-msvc@0.72.0': optional: true - '@rolldown/pluginutils@1.0.1': {} + '@oxfmt/binding-win32-x64-msvc@0.72.0': + optional: true - '@rollup/rollup-android-arm-eabi@4.60.4': + '@oxlint-tsgolint/darwin-arm64@7.0.2003': optional: true - '@rollup/rollup-android-arm64@4.60.4': + '@oxlint-tsgolint/darwin-x64@7.0.2003': optional: true - '@rollup/rollup-darwin-arm64@4.60.4': + '@oxlint-tsgolint/linux-arm64@7.0.2003': optional: true - '@rollup/rollup-darwin-x64@4.60.4': + '@oxlint-tsgolint/linux-x64@7.0.2003': optional: true - '@rollup/rollup-freebsd-arm64@4.60.4': + '@oxlint-tsgolint/win32-arm64@7.0.2003': optional: true - '@rollup/rollup-freebsd-x64@4.60.4': + '@oxlint-tsgolint/win32-x64@7.0.2003': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + '@oxlint/binding-android-arm-eabi@1.87.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.4': + '@oxlint/binding-android-arm64@1.87.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.4': + '@oxlint/binding-darwin-arm64@1.87.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.4': + '@oxlint/binding-darwin-x64@1.87.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.4': + '@oxlint/binding-freebsd-x64@1.87.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.4': + '@oxlint/binding-linux-arm-gnueabihf@1.87.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.4': + '@oxlint/binding-linux-arm-musleabihf@1.87.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.4': + '@oxlint/binding-linux-arm64-gnu@1.87.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.4': + '@oxlint/binding-linux-arm64-musl@1.87.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.4': + '@oxlint/binding-linux-ppc64-gnu@1.87.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.4': + '@oxlint/binding-linux-riscv64-gnu@1.87.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.4': + '@oxlint/binding-linux-riscv64-musl@1.87.0': optional: true - '@rollup/rollup-linux-x64-musl@4.60.4': + '@oxlint/binding-linux-s390x-gnu@1.87.0': optional: true - '@rollup/rollup-openbsd-x64@4.60.4': + '@oxlint/binding-linux-x64-gnu@1.87.0': optional: true - '@rollup/rollup-openharmony-arm64@4.60.4': + '@oxlint/binding-linux-x64-musl@1.87.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.4': + '@oxlint/binding-openharmony-arm64@1.87.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.4': + '@oxlint/binding-win32-arm64-msvc@1.87.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.4': + '@oxlint/binding-win32-ia32-msvc@1.87.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.4': + '@oxlint/binding-win32-x64-msvc@1.87.0': optional: true - '@standard-schema/spec@1.1.0': {} + '@oxlint/plugins@1.79.0': {} + + '@polka/url@1.0.0-next.29': {} - '@tybys/wasm-util@0.10.4': + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': dependencies: - tslib: 2.8.1 - optional: true + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.10 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/user-event@14.6.8(@testing-library/dom@10.4.2)': + dependencies: + '@testing-library/dom': 10.4.2 + + '@types/aria-query@5.0.4': {} '@types/chai@5.2.3': dependencies: @@ -2781,8 +2489,6 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/estree@1.0.8': {} - '@types/estree@1.0.9': {} '@types/node@22.19.19': @@ -2797,52 +2503,194 @@ snapshots: '@vercel/functions@1.6.0': {} - '@vitest/expect@4.1.11': + '@vitest/browser-preview@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3)': dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.8(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + ws: 8.22.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.14.6(@types/node@22.19.19)(typescript@5.5.4) + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)' + + '@vitest/pretty-format@5.0.3': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/snapshot@5.0.3': + dependencies: + '@vitest/pretty-format': 5.0.3 + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pathe: 2.0.3 + + '@vitest/spy@5.0.3': {} + + '@vitest/ui@5.0.3(vitest@5.0.3)': + dependencies: + '@vitest/utils': 5.0.3 + fflate: 0.8.3 + flatted: 3.4.4 + pathe: 2.0.3 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + + '@vitest/utils@5.0.3': + dependencies: + '@vitest/pretty-format': 5.0.3 + convert-source-map: 2.0.0 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.11(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7))': - dependencies: - '@vitest/spy': 4.1.11 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - msw: 2.14.6(@types/node@22.19.19)(typescript@5.5.4) - vite: 8.0.14(@types/node@22.19.19)(esbuild@0.27.7) + '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)': + dependencies: + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.28 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@types/node': 22.19.19 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + typescript: 5.5.4 + + '@voidzero-dev/vite-plus-darwin-arm64@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-darwin-x64@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-musl@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-gnu@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-win32-x64-msvc@1.1.0': + optional: true + + '@yuku-codegen/binding-android-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.9.5': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-win32-x64@0.9.5': + optional: true + + '@yuku-parser/binding-android-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-darwin-x64@0.9.5': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.9.5': + optional: true - '@vitest/pretty-format@4.1.11': - dependencies: - tinyrainbow: 3.1.1 + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + optional: true - '@vitest/runner@4.1.11': - dependencies: - '@vitest/utils': 4.1.11 - pathe: 2.0.3 + '@yuku-parser/binding-linux-arm64-musl@0.9.5': + optional: true - '@vitest/snapshot@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pathe: 2.0.3 + '@yuku-parser/binding-linux-x64-gnu@0.9.5': + optional: true - '@vitest/spy@4.1.11': {} + '@yuku-parser/binding-linux-x64-musl@0.9.5': + optional: true - '@vitest/utils@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 + '@yuku-parser/binding-win32-arm64@0.9.5': + optional: true - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 + '@yuku-parser/binding-win32-x64@0.9.5': + optional: true + + '@yuku-toolchain/types@0.9.5': {} acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: @@ -2865,76 +2713,38 @@ snapshots: dependencies: color-convert: 2.0.1 - any-promise@1.3.0: {} + ansi-styles@5.2.0: {} - argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 assertion-error@2.0.1: {} astring@1.9.0: {} - balanced-match@4.0.4: {} - - body-parser@2.3.0: - dependencies: - bytes: 3.1.2 - content-type: 2.0.0 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.15.2 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - - brace-expansion@5.0.9: - dependencies: - balanced-match: 4.0.4 - braintrust@file:../../../js(zod@4.4.3): dependencies: - '@next/env': 14.2.35 '@vercel/functions': 1.6.0 acorn: 8.16.0 acorn-import-attributes: 1.9.5(acorn@8.16.0) ajv: 8.20.0 - argparse: 2.0.1 astring: 1.9.0 cjs-module-lexer: 2.2.0 - cli-progress: 3.12.0 - cli-table3: 0.6.5 - cors: 2.8.6 dotenv: 16.6.1 - esbuild: 0.28.1 esquery: 1.7.0 eventsource-parser: 1.1.2 - express: 5.2.1 - http-errors: 2.0.1 meriyah: 6.1.4 - minimatch: 10.2.5 module-details-from-path: 1.0.4 mustache: 4.2.0 - pluralize: 8.0.0 semifies: 1.0.0 source-map: 0.7.6 - termi-link: 1.1.0 unplugin: 2.3.11 uuid: 11.1.1 zod: 4.4.3 zod-to-json-schema: 3.25.2(zod@4.4.3) - optionalDependencies: - '@braintrust/bt-darwin-arm64': 0.12.0 - '@braintrust/bt-darwin-x64': 0.12.0 - '@braintrust/bt-linux-arm64': 0.12.0 - '@braintrust/bt-linux-x64': 0.12.0 - '@braintrust/bt-linux-x64-musl': 0.12.0 - '@braintrust/bt-win32-arm64': 0.12.0 - '@braintrust/bt-win32-x64': 0.12.0 transitivePeerDependencies: - '@aws-sdk/credential-provider-web-identity' - - supports-color bufrw@1.4.0: dependencies: @@ -2943,43 +2753,10 @@ snapshots: hexer: 1.5.0 xtend: 4.0.2 - bundle-require@5.1.0(esbuild@0.27.7): - dependencies: - esbuild: 0.27.7 - load-tsconfig: 0.2.5 - - bytes@3.1.2: {} - - cac@6.7.14: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - chai@6.2.2: {} - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - cjs-module-lexer@2.2.0: {} - cli-progress@3.12.0: - dependencies: - string-width: 4.2.3 - - cli-table3@0.6.5: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - cli-width@4.1.0: {} cliui@8.0.1: @@ -2994,97 +2771,33 @@ snapshots: color-name@1.1.4: {} - commander@4.1.1: {} - - confbox@0.1.8: {} - - consola@3.4.2: {} - - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - - content-type@2.0.0: {} - convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - cookie@1.1.1: {} - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - debug@4.4.3: dependencies: ms: 2.1.3 - depd@2.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} - dotenv@16.6.1: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 + dom-accessibility-api@0.5.16: {} - ee-first@1.1.1: {} + dotenv@16.6.1: {} emoji-regex@8.0.0: {} - encodeurl@2.0.0: {} - error@7.0.2: dependencies: string-template: 0.2.1 xtend: 4.0.2 - es-define-property@1.0.1: {} - es-errors@1.3.0: {} es-module-lexer@2.3.2: {} - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -3113,11 +2826,10 @@ snapshots: '@esbuild/win32-arm64': 0.28.1 '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + optional: true escalade@3.2.0: {} - escape-html@1.0.3: {} - esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -3128,45 +2840,10 @@ snapshots: dependencies: '@types/estree': 1.0.9 - etag@1.8.1: {} - eventsource-parser@1.1.2: {} expect-type@1.4.0: {} - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.3.0 - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.2 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - fast-deep-equal@3.1.3: {} fast-string-truncated-width@3.0.3: {} @@ -3185,26 +2862,9 @@ snapshots: optionalDependencies: picomatch: 4.0.7 - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.2 - rollup: 4.60.4 - - forwarded@0.2.0: {} + fflate@0.8.3: {} - fresh@2.0.0: {} + flatted@3.4.4: {} fsevents@2.3.3: optional: true @@ -3213,30 +2873,8 @@ snapshots: get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.3 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 - - gopd@1.2.0: {} - graphql@16.14.0: {} - has-symbols@1.1.0: {} - hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -3253,22 +2891,6 @@ snapshots: process: 0.10.1 xtend: 4.0.2 - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - - inherits@2.0.4: {} - - ipaddr.js@1.9.1: {} - is-core-module@2.16.2: dependencies: hasown: 2.0.3 @@ -3277,8 +2899,6 @@ snapshots: is-node-process@1.2.0: {} - is-promise@4.0.0: {} - jaeger-client@3.19.0: dependencies: node-int64: 0.4.0 @@ -3287,7 +2907,7 @@ snapshots: uuid: 11.1.1 xorshift: 1.2.0 - joycon@3.1.1: {} + js-tokens@4.0.0: {} json-schema-traverse@1.0.0: {} @@ -3340,12 +2960,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - lodash.camelcase@4.3.0: {} lodash.merge@4.6.2: {} @@ -3354,39 +2968,20 @@ snapshots: long@5.3.2: {} - magic-string@0.30.21: + lz-string@1.5.0: {} + + magic-string@1.4.3: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 - math-intrinsics@1.1.0: {} - - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - meriyah@6.1.4: {} - mime-db@1.54.0: {} - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.9 - minimist@1.2.8: {} - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.4 - module-details-from-path@1.0.4: {} + mrmime@2.0.1: {} + ms@2.1.3: {} msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4): @@ -3418,65 +3013,85 @@ snapshots: mute-stream@4.0.0: {} - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - nanoid@3.3.19: {} - negotiator@1.0.0: {} - node-int64@0.4.0: {} - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - obug@2.2.1: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - opentracing@0.14.7: {} outvariant@1.4.3: {} - parseurl@1.3.3: {} + oxfmt@0.72.0(vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4)): + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4) + + oxlint-tsgolint@7.0.2003: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 7.0.2003 + '@oxlint-tsgolint/darwin-x64': 7.0.2003 + '@oxlint-tsgolint/linux-arm64': 7.0.2003 + '@oxlint-tsgolint/linux-x64': 7.0.2003 + '@oxlint-tsgolint/win32-arm64': 7.0.2003 + '@oxlint-tsgolint/win32-x64': 7.0.2003 + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4) path-parse@1.0.7: {} path-to-regexp@6.3.0: {} - path-to-regexp@8.4.2: {} - pathe@2.0.3: {} picocolors@1.1.1: {} picomatch@4.0.7: {} - pirates@4.0.7: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - pluralize@8.0.0: {} - - postcss-load-config@6.0.1(postcss@8.5.28): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - postcss: 8.5.28 + pngjs@7.0.0: {} postcss@8.5.28: dependencies: @@ -3484,6 +3099,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + process@0.10.1: {} protobufjs@7.6.5: @@ -3500,25 +3121,7 @@ snapshots: '@types/node': 22.19.19 long: 5.3.2 - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - qs@6.15.2: - dependencies: - side-channel: 1.1.0 - - range-parser@1.2.1: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - - readdirp@4.1.2: {} + react-is@17.0.2: {} require-directory@2.1.1: {} @@ -3532,8 +3135,6 @@ snapshots: transitivePeerDependencies: - supports-color - resolve-from@5.0.0: {} - resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -3543,143 +3144,26 @@ snapshots: rettime@0.11.11: {} - rolldown@1.0.2: - dependencies: - '@oxc-project/types': 0.132.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 - - rollup@4.60.4: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.4 - '@rollup/rollup-android-arm64': 4.60.4 - '@rollup/rollup-darwin-arm64': 4.60.4 - '@rollup/rollup-darwin-x64': 4.60.4 - '@rollup/rollup-freebsd-arm64': 4.60.4 - '@rollup/rollup-freebsd-x64': 4.60.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 - '@rollup/rollup-linux-arm-musleabihf': 4.60.4 - '@rollup/rollup-linux-arm64-gnu': 4.60.4 - '@rollup/rollup-linux-arm64-musl': 4.60.4 - '@rollup/rollup-linux-loong64-gnu': 4.60.4 - '@rollup/rollup-linux-loong64-musl': 4.60.4 - '@rollup/rollup-linux-ppc64-gnu': 4.60.4 - '@rollup/rollup-linux-ppc64-musl': 4.60.4 - '@rollup/rollup-linux-riscv64-gnu': 4.60.4 - '@rollup/rollup-linux-riscv64-musl': 4.60.4 - '@rollup/rollup-linux-s390x-gnu': 4.60.4 - '@rollup/rollup-linux-x64-gnu': 4.60.4 - '@rollup/rollup-linux-x64-musl': 4.60.4 - '@rollup/rollup-openbsd-x64': 4.60.4 - '@rollup/rollup-openharmony-arm64': 4.60.4 - '@rollup/rollup-win32-arm64-msvc': 4.60.4 - '@rollup/rollup-win32-ia32-msvc': 4.60.4 - '@rollup/rollup-win32-x64-gnu': 4.60.4 - '@rollup/rollup-win32-x64-msvc': 4.60.4 - fsevents: 2.3.3 - - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - - safer-buffer@2.1.2: {} - semifies@1.0.0: {} semver@7.8.1: {} - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - set-cookie-parser@3.1.0: {} - setprototypeof@1.2.0: {} - shimmer@1.2.1: {} - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 + signal-exit@4.1.0: {} - side-channel@1.1.0: + sirv@3.0.2: dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - siginfo@2.0.0: {} - - signal-exit@4.1.0: {} + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 source-map-js@1.2.1: {} source-map@0.7.6: {} - stackback@0.0.2: {} - statuses@2.0.2: {} std-env@4.2.0: {} @@ -3698,52 +3182,27 @@ snapshots: dependencies: ansi-regex: 5.0.1 - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.16 - ts-interface-checker: 0.1.13 - supports-preserve-symlinks-flag@1.0.0: {} tagged-tag@1.0.0: {} - termi-link@1.1.0: {} - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - thriftrw@3.11.4: dependencies: bufrw: 1.4.0 error: 7.0.2 long: 2.4.0 - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} + tinybench@6.2.2: {} tinyexec@1.3.1: {} - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.7) - picomatch: 4.0.7 - tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinypool@2.2.0: {} + tinyrainbow@3.1.1: {} tldts-core@7.4.0: {} @@ -3752,65 +3211,20 @@ snapshots: dependencies: tldts-core: 7.4.0 - toidentifier@1.0.1: {} + totalist@3.0.1: {} tough-cookie@6.0.1: dependencies: tldts: 7.4.0 - tree-kill@1.2.2: {} - - ts-interface-checker@0.1.13: {} - - tslib@2.8.1: - optional: true - - tsup@8.5.1(postcss@8.5.28)(typescript@5.5.4): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.7) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.7 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.28) - resolve-from: 5.0.0 - rollup: 4.60.4 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.16 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.28 - typescript: 5.5.4 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - type-fest@5.6.0: dependencies: tagged-tag: 1.0.0 - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - typescript@5.5.4: {} - ufo@1.6.4: {} - undici-types@6.21.0: {} - unpipe@1.0.0: {} - unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -3822,54 +3236,92 @@ snapshots: uuid@11.1.1: {} - vary@1.1.2: {} - - vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.7 - postcss: 8.5.28 - rolldown: 1.0.2 - tinyglobby: 0.2.17 + vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4): + dependencies: + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)' + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) optionalDependencies: - '@types/node': 22.19.19 - esbuild: 0.27.7 - fsevents: 2.3.3 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc + - yaml - vitest@4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)): + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.3 obug: 2.2.1 - pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 + tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.0.14(@types/node@22.19.19)(esbuild@0.27.7) - why-is-node-running: 2.3.0 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)' + why-is-node-running: 3.2.1 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.19 + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) transitivePeerDependencies: - msw webpack-virtual-modules@0.6.2: {} - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 + why-is-node-running@3.2.1: {} wrap-ansi@7.0.0: dependencies: @@ -3877,7 +3329,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} + ws@8.22.0: {} xorshift@1.2.0: {} @@ -3897,6 +3349,45 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yuku-ast@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + + yuku-codegen@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.9.5 + '@yuku-codegen/binding-darwin-arm64': 0.9.5 + '@yuku-codegen/binding-darwin-x64': 0.9.5 + '@yuku-codegen/binding-freebsd-x64': 0.9.5 + '@yuku-codegen/binding-linux-arm-gnu': 0.9.5 + '@yuku-codegen/binding-linux-arm-musl': 0.9.5 + '@yuku-codegen/binding-linux-arm64-gnu': 0.9.5 + '@yuku-codegen/binding-linux-arm64-musl': 0.9.5 + '@yuku-codegen/binding-linux-x64-gnu': 0.9.5 + '@yuku-codegen/binding-linux-x64-musl': 0.9.5 + '@yuku-codegen/binding-win32-arm64': 0.9.5 + '@yuku-codegen/binding-win32-x64': 0.9.5 + + yuku-parser@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + yuku-ast: 0.9.5 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.9.5 + '@yuku-parser/binding-darwin-arm64': 0.9.5 + '@yuku-parser/binding-darwin-x64': 0.9.5 + '@yuku-parser/binding-freebsd-x64': 0.9.5 + '@yuku-parser/binding-linux-arm-gnu': 0.9.5 + '@yuku-parser/binding-linux-arm-musl': 0.9.5 + '@yuku-parser/binding-linux-arm64-gnu': 0.9.5 + '@yuku-parser/binding-linux-arm64-musl': 0.9.5 + '@yuku-parser/binding-linux-x64-gnu': 0.9.5 + '@yuku-parser/binding-linux-x64-musl': 0.9.5 + '@yuku-parser/binding-win32-arm64': 0.9.5 + '@yuku-parser/binding-win32-x64': 0.9.5 + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/integrations/otel-js/otel-v1/tests/otel-v1.test.ts b/integrations/otel-js/otel-v1/tests/otel-v1.test.ts index a6f7e8517..c31b8e320 100644 --- a/integrations/otel-js/otel-v1/tests/otel-v1.test.ts +++ b/integrations/otel-js/otel-v1/tests/otel-v1.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/consistent-type-assertions */ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi } from "vite-plus/test"; import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base"; import { BraintrustExporter } from "../src/otel"; diff --git a/integrations/otel-js/otel-v1/vitest.config.ts b/integrations/otel-js/otel-v1/vite.config.ts similarity index 93% rename from integrations/otel-js/otel-v1/vitest.config.ts rename to integrations/otel-js/otel-v1/vite.config.ts index 6c805c533..3d8be82c0 100644 --- a/integrations/otel-js/otel-v1/vitest.config.ts +++ b/integrations/otel-js/otel-v1/vite.config.ts @@ -1,5 +1,5 @@ import { readFileSync } from "node:fs"; -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vite-plus"; import { detectOtelVersion, logOtelVersions, diff --git a/integrations/otel-js/otel-v2/package.json b/integrations/otel-js/otel-v2/package.json index 3b981e2e2..c1169bc32 100644 --- a/integrations/otel-js/otel-v2/package.json +++ b/integrations/otel-js/otel-v2/package.json @@ -3,24 +3,10 @@ "version": "0.0.1-test", "private": true, "description": "Test package for @braintrust/otel with OpenTelemetry 2.x", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, "scripts": { - "build": "tsup", - "watch": "tsup --watch", "clean": "rm -rf dist/*", - "build:deps": "(cd ../../.. && pnpm turbo run build --filter=braintrust --filter=@braintrust/otel)", - "test": "pnpm run build:deps && vitest run" + "build:deps": "(cd ../../.. && pnpm exec vp run --filter=braintrust --filter=@braintrust/otel build)", + "test": "pnpm run build:deps && vp test run" }, "author": "Braintrust Data Inc.", "license": "MIT", @@ -36,9 +22,10 @@ "@types/node": "^22.15.21", "braintrust": "file:../../../js", "msw": "^2.6.6", - "tsup": "^8.5.0", "typescript": "5.5.4", - "vitest": "4.1.11" + "vitest": "5.0.3", + "vite-plus": "1.1.0", + "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0" }, "pnpm": { "overrides": { diff --git a/integrations/otel-js/otel-v2/pnpm-lock.yaml b/integrations/otel-js/otel-v2/pnpm-lock.yaml index b777da861..79e1f9169 100644 --- a/integrations/otel-js/otel-v2/pnpm-lock.yaml +++ b/integrations/otel-js/otel-v2/pnpm-lock.yaml @@ -60,58 +60,39 @@ importers: msw: specifier: ^2.6.6 version: 2.14.6(@types/node@22.19.19)(typescript@5.5.4) - tsup: - specifier: ^8.5.0 - version: 8.5.1(postcss@8.5.28)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 + vite: + specifier: npm:@voidzero-dev/vite-plus-core@1.1.0 + version: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)' + vite-plus: + specifier: 1.1.0 + version: 1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4) vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)) + specifier: 5.0.3 + version: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) packages: - '@braintrust/bt-darwin-arm64@0.12.0': - resolution: {integrity: sha512-mY6VW/3VwcOQOGN8sYHS6F0xzHTFwgZcNlj7zlQttI6OXOCGt/bhonGIqd03QBhxmj0M31ymSS7TqSeCK/RYIQ==} - cpu: [arm64] - os: [darwin] - - '@braintrust/bt-darwin-x64@0.12.0': - resolution: {integrity: sha512-woyRyDv2DfCF8+von+3X9f1cddAbFnKLSfCbEkrBQVc0ZsAM60as8nehYv7DkafJF/67yKFx/sUraV65sukesQ==} - cpu: [x64] - os: [darwin] - - '@braintrust/bt-linux-arm64@0.12.0': - resolution: {integrity: sha512-J9/7f3EIMKmFmSSQHQnAQLpUgB8YJENrOlkABjlTprBgsIYVgz7tSH7yg2P3ur+2Jem7PpGnrDxHGh/UjRfjsg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@braintrust/bt-linux-x64-musl@0.12.0': - resolution: {integrity: sha512-KnLgENOoztBXcH+mLFJe4bYzi67kSOuELOQrm4Hler35GjgaBMI1fFLIUjKRPAmyT9VIhBMhy1ICfGEFLueMEQ==} - cpu: [x64] - os: [linux] - libc: [musl] + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} - '@braintrust/bt-linux-x64@0.12.0': - resolution: {integrity: sha512-HJFbUl3HYYY1Ivw8OYBeIMcIsU9r7+fC9BzLWB5FdH7881ToKee2wbbLZssMCxFbMVeUxzEo+SzGD/1Z9Gk9CA==} - cpu: [x64] - os: [linux] - libc: [glibc] + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} - '@braintrust/bt-win32-arm64@0.12.0': - resolution: {integrity: sha512-+7PkdBAmiqEJsWteGHP/Zs62F75PkHPA8m/ej4TOz/mHGKulUU0bZibw91KRqIB7J7jGi5ItvCiqkiGaLKLnyw==} - cpu: [arm64] - os: [win32] + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} - '@braintrust/bt-win32-x64@0.12.0': - resolution: {integrity: sha512-Q0c+pVUGm82n/7N8QJ5s6j/G/Q0GFzqOaTipHjkmElLbAOOEcfRH/uljdtIPNBiEQcrzqirS0GmOgiFkeQ3Txg==} - cpu: [x64] - os: [win32] + '@blazediff/core@1.10.0': + resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==} '@braintrust/otel@file:..': resolution: {directory: .., type: directory} + engines: {node: '>=22.18.0'} peerDependencies: '@opentelemetry/api': '>=1.9.0' '@opentelemetry/core': '>=1.9.0' @@ -119,325 +100,156 @@ packages: '@opentelemetry/sdk-trace-base': '>=1.9.0' braintrust: '>=1.0.0-0' - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} @@ -501,6 +313,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -511,16 +326,6 @@ packages: resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} - '@napi-rs/wasm-runtime@1.2.4': - resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - - '@next/env@14.2.35': - resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -701,292 +506,345 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} - - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + '@oxc-project/runtime@0.153.0': + resolution: {integrity: sha512-rXJvtlsVG4xJNq64hiaUTIatE7jbBgpa6l4QgmEa2jLgT3ULH9scUBkpsC9N1DfmqixLEyT6DuK6OSXozziIlw==} + engines: {node: ^20.19.0 || >=22.12.0} - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + '@oxc-project/types@0.153.0': + resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@oxfmt/binding-android-arm-eabi@0.72.0': + resolution: {integrity: sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + '@oxfmt/binding-android-arm64@0.72.0': + resolution: {integrity: sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + '@oxfmt/binding-darwin-arm64@0.72.0': + resolution: {integrity: sha512-LjQ8tlevdtwraFlWC6AnQ1Forzde0clRmnsP3QmK+ydSElDt/J+5Jc+m+//Ly1G97aP7WCNUw110QMIUw2K7xA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + '@oxfmt/binding-darwin-x64@0.72.0': + resolution: {integrity: sha512-+ZbcxhB1C34RQR5IldSyY6Z/yalJIxHXhP36PGE4sEq0f/lgL5jCLmOKoDeVm8dqd9gFGDsd1bNWHSGCAwdSkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + '@oxfmt/binding-freebsd-x64@0.72.0': + resolution: {integrity: sha512-yGPhA8sePEBFV9iGcprhyOk3Bz3fZshswMH3DpYIPZFLT1DG5LjiZ0j6AJQpSueaL6bUK0av1K+5+fe+ldy5EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + '@oxfmt/binding-linux-arm-gnueabihf@0.72.0': + resolution: {integrity: sha512-/g5axuqFMcAtoQrFLR5EJEMDjR5Ea4GuzpdDmMpPWAgrF1DmL4kD1NPNVlpXEOxpFJnD6lPvLKYj1lzdlrfifA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.72.0': + resolution: {integrity: sha512-InVitdHbS09PM+w+WinIiyX37rC9R3L7lUP9mKy9uKTEDSUFaLjWsLoreRIxKivnv7Ndqc6Trvi+K+b6ZzhiJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + '@oxfmt/binding-linux-arm64-gnu@0.72.0': + resolution: {integrity: sha512-WKjlyRAxpPjXoqeuwFBim5LscgvY+RNEiZ971C9JlRQZONhJTsXDki/oHFYQE+fzzRpzXh3owZvOdT3kNL2g+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + '@oxfmt/binding-linux-arm64-musl@0.72.0': + resolution: {integrity: sha512-poafnbsPpJhR+5Mbwpf8CscugTyiRAzjTOxRVn8ra3X2jnURs2xL5Du2xl5g2iW85lICMglpmLf8jDjZEbNgzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + '@oxfmt/binding-linux-ppc64-gnu@0.72.0': + resolution: {integrity: sha512-zNUi0gKRsO7S3IfPzsTbnm88OedPuX32hmZFWZRWHvWJlAoZRwVbeO6Nb3hng4vDgL7qUKp4TGqzPz1mhsycYg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + '@oxfmt/binding-linux-riscv64-gnu@0.72.0': + resolution: {integrity: sha512-qTY7z/iyF/rWI2HWwWMevXqZW5PZUOofUy2afK7v6rseXnpld3yjgyIk6pCux3DqWQ0lB+f5atvLJNBwzLvFsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.72.0': + resolution: {integrity: sha512-w18qfo8vEMD6EKEHtzTvjc6jH8yCyXIT1Bn7PFWJd0PpZyzH/VQDx4xvjCh0LGz6GRzqiezbisTdG9AoXtpkZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.72.0': + resolution: {integrity: sha512-grW8HwEir+vK7fCcq1ZgL3NHQCfZ1PidiERE9Hbf+24xGys+DoDaoqTht4ccfn0PcBwbViLkwkda/KNpJu2KRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + '@oxfmt/binding-linux-x64-gnu@0.72.0': + resolution: {integrity: sha512-wQJRQfWBRIn88Pkr449EUViwRayrYUScg7ty+Ihiem21mQ9BUA6S9e/4RAgyNk9lMag0KBouyKskjq61rOC0WQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + '@oxfmt/binding-linux-x64-musl@0.72.0': + resolution: {integrity: sha512-T1B0uQzafxuGPjjaPrZJ497nxoi1BiI65JfsYc56nNpmev3msfb/iaTmH2kQuNFqa8PKxpMlf3GhWrWBvBPORQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + '@oxfmt/binding-openharmony-arm64@0.72.0': + resolution: {integrity: sha512-OKxxgTurl7+hypVplO8QRAbbQlwEs0j1Y6FZv+ZN2qNbdxQk2muMlzuJNBGTCWwxQ0rkkOD1uK/DW1fvzoUSJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + '@oxfmt/binding-win32-arm64-msvc@0.72.0': + resolution: {integrity: sha512-L78TKzURqvjuZOCo88ixMbPXf/RP7u31Y0cmjJ/KEmrHmfYhmhEvsDaTYPiyTRne64jUgCbADfY0YeoQk4/Xyw==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] + cpu: [arm64] + os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + '@oxfmt/binding-win32-ia32-msvc@0.72.0': + resolution: {integrity: sha512-gQtFW+Ii8BWgtJTAD17TY/WsdNS5gUfAPov8iI1jeq4r9lRRYImmxPL08UU00/RLaCfBJwh2UgINnDY0iOiZ+A==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [ia32] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + '@oxfmt/binding-win32-x64-msvc@0.72.0': + resolution: {integrity: sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@oxlint-tsgolint/darwin-arm64@7.0.2003': + resolution: {integrity: sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@7.0.2003': + resolution: {integrity: sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@7.0.2003': + resolution: {integrity: sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@7.0.2003': + resolution: {integrity: sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@7.0.2003': + resolution: {integrity: sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@7.0.2003': + resolution: {integrity: sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==} + cpu: [x64] + os: [win32] - '@rollup/rollup-android-arm-eabi@4.60.4': - resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + '@oxlint/binding-android-arm-eabi@1.87.0': + resolution: {integrity: sha512-22tVIHlB9898OYsBKmYB6He1MCSqdTmK1BVWuSNUTVrpS0KWNege+U9uEYqGJ+KatQag0r/oU44i3OOs46z7HQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.4': - resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + '@oxlint/binding-android-arm64@1.87.0': + resolution: {integrity: sha512-JfZQ3MWf74n8ov0DojZpkOduIubYJvZL5tbTMMhDutsal6u1xH1QuNpN+lK+EQwF8AeAfgwTR2fgh5U2br27XA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.4': - resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + '@oxlint/binding-darwin-arm64@1.87.0': + resolution: {integrity: sha512-rkx6Xvvw93z2GImVOyvbZwAZcoY0Z6MJW177XNsSB+i/CBsH7ANyelTp01VZyIkM3iSXtqKVb4+ptBgPHap5pQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.4': - resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + '@oxlint/binding-darwin-x64@1.87.0': + resolution: {integrity: sha512-cw5nYw7L/pQ2SgGBgOcg/Qo89llQre3vIc/kuBLhSICzu1r5s8L76H3ug6AIFsD98WpROj8/z5JVzkf4daxHaQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.4': - resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.4': - resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + '@oxlint/binding-freebsd-x64@1.87.0': + resolution: {integrity: sha512-x/SuznE6loRZK9gz9AS4ZmzNzSu+5KdeoCS+sVHhnrE8HM6AD8UyTjwldQTTqlXGvkwHASgB0NiTNOc3o3PdAg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + '@oxlint/binding-linux-arm-gnueabihf@1.87.0': + resolution: {integrity: sha512-+JEDmCAXiiFmKhAjZhgAOwaHWY6WENOQ5QA+lFuY59tgsUCDOTRi46B+lENK6CWvtxsXpwkZXNOgpjAQcpIpnA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + '@oxlint/binding-linux-arm-musleabihf@1.87.0': + resolution: {integrity: sha512-/uC9XqXLAC9AHFLNycwmSQEvKqGrOCgU+uXALI7gNW0DG81TJ/3T2eHnKdRvcb5R1gFEfjxDwI4e4zIa29rICQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.4': - resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + '@oxlint/binding-linux-arm64-gnu@1.87.0': + resolution: {integrity: sha512-FTRYPsBy/M728vnUv7l+KAwSAa3Qi4BQbhVvy7NYuMTZ0sUa/OWsbMx79HVFw6u/5KWcaYayDsoOiv7xbevzDg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.4': - resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + '@oxlint/binding-linux-arm64-musl@1.87.0': + resolution: {integrity: sha512-6sC+N7IY0h1egd8SrYUGNN7CUyKJZ4bIZakKa7V7hfinf4jfi8Kq4+klJvFp5DqXcwMn44I5MeBwmJluO1odFg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.4': - resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.4': - resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + '@oxlint/binding-linux-ppc64-gnu@1.87.0': + resolution: {integrity: sha512-d5Mnn+ICbhoJPi37Ccjy+86T5qfH2+mSTrTMBXNPfB+wqSWOayBBLLIrCXdUy1YssluCVdGkl+DO9w5r5ODmUA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.4': - resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + '@oxlint/binding-linux-riscv64-gnu@1.87.0': + resolution: {integrity: sha512-sf5YWuUo1Dx9loqWCgF8P278MycfcD4JHjDC/LV5j11L3Lqa4PFiwCjD6B8kP4AjsgZZkLn3xbpJEtZBaccs4Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.4': - resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + '@oxlint/binding-linux-riscv64-musl@1.87.0': + resolution: {integrity: sha512-+aCraYbmK3uU3P0rFojjeaK9pGY6JvBDXh/0CM+gPyWKmNmXmnFe0qj4qj02U4ibFpbd9Pa9CmDvIdjJUd29rA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.4': - resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + '@oxlint/binding-linux-s390x-gnu@1.87.0': + resolution: {integrity: sha512-7rzbZQIljo6VLWVPqUyLa2P/WV3VFsJB4M52R4NCUJn1A50yjtUsHxiMHFL9opEKGFIiTbZ3VQzmtcJOPUq4NQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.4': - resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + '@oxlint/binding-linux-x64-gnu@1.87.0': + resolution: {integrity: sha512-Dry46Fa+AXE/xt9J/YriujU1pU9mzfNWHGQwtOYibAFH8HLrKWCi93AhIGnuUMiwn2JbuBkRyz51f50Ly4r4ow==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.4': - resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + '@oxlint/binding-linux-x64-musl@1.87.0': + resolution: {integrity: sha512-f/HMZez74gJvnRpGz+b5sdGyWrEgRLmFTXg9sNoqjPWogGxYHUhXKTKyMiYsu9rUGR2ciYqL3fk+Ua4JhMJbRA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.4': - resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.4': - resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + '@oxlint/binding-openharmony-arm64@1.87.0': + resolution: {integrity: sha512-lEC6ZfPZm/07JlB7FsOWX9o1RiHlXiBW594LSUGuXmekC8LZmvUPP9XzE2WZcNxJi5cHA/Fw56o02c3JxgMA5g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.4': - resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + '@oxlint/binding-win32-arm64-msvc@1.87.0': + resolution: {integrity: sha512-afBWKOD36dUiA9AenaiWpCg8xfyUVPYznyAAVXu413brK4RA5UI/Xoc4yfeGIT8G1a44MV+JR7hioryerBDzGQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.4': - resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + '@oxlint/binding-win32-ia32-msvc@1.87.0': + resolution: {integrity: sha512-daptNsEOuru4/nJTxV4DBHNJ5GepzmaBVQf+jjgKQGi9NRlZKik+RFq4hUL7ewsF1pwjKM+SkYaMbF82UwFGnw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.4': - resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + '@oxlint/binding-win32-x64-msvc@1.87.0': + resolution: {integrity: sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.4': - resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} - cpu: [x64] - os: [win32] + '@oxlint/plugins@1.79.0': + resolution: {integrity: sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@tybys/wasm-util@0.10.4': - resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - '@types/node@22.19.19': - resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/user-event@14.6.8': + resolution: {integrity: sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.19.19': + resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} @@ -1006,11 +864,18 @@ packages: '@aws-sdk/credential-provider-web-identity': optional: true - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + '@vitest/browser-preview@5.0.3': + resolution: {integrity: sha512-jfnEBv5yDRPR1MeiTlTW5bcZoPsiI7MBhpZR8IJiSxVhdK6Pa4gPk3tLHTRxVHy24kkWxY2TnUbnszU3J1bQJw==} + peerDependencies: + vitest: 5.0.3 + + '@vitest/browser@5.0.3': + resolution: {integrity: sha512-7KHIkVA/gTRxQV9qPa9qLK+g5INZG6iRrlwXSFG5EKsOomW3UzzW7wsdt1Yp5PufzQ43lniuONlb4lUVCC2qSg==} + peerDependencies: + vitest: 5.0.3 - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} peerDependencies: msw: ^2.4.9 vite: ^8.0.5 @@ -1020,24 +885,302 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + '@vitest/pretty-format@5.0.3': + resolution: {integrity: sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==} + + '@vitest/snapshot@5.0.3': + resolution: {integrity: sha512-SK4YTg9jL+jwakHIZxfciuvyOQsbY15AueR3USGtsXzmy+3FbJgqjAbC+ac20mRLwfEDtO5sfTqK6bUCDTRVPA==} + + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} + + '@vitest/ui@5.0.3': + resolution: {integrity: sha512-9dfvqDBvSbHM6y9YVXxAypkl2z7mrWozK/JFo237qI0cj/3M/kzDKiVZqzi3TE54Ler7Jditud7OMBZCyUvRHA==} + peerDependencies: + vitest: 5.0.3 + + '@vitest/utils@5.0.3': + resolution: {integrity: sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==} + + '@voidzero-dev/vite-plus-core@1.1.0': + resolution: {integrity: sha512-Ai6HQP1zG0H3ZrmStEaMVKtDzW0xU3VgMzm3LFaXDQMEp8nAqvp0VGoU8HmpB9xv9vDSl+UxiSNOpuY9T714OQ==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@types/node': ^20.19.0 || >=22.12.0 + '@typescript/native-preview': '*' + '@vitejs/devtools': ^0.7.1 + '@volar/typescript': ~2.4.0 + '@vue/language-core': ~3.2.0 || ~3.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + publint: ^0.3.8 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: '>=0.5.0' + unrun: '*' + vue-tsc: ~3.2.0 || ~3.3.0 + yaml: ^2.4.2 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@types/node': + optional: true + '@typescript/native-preview': + optional: true + '@vitejs/devtools': + optional: true + '@volar/typescript': + optional: true + '@vue/language-core': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + vue-tsc: + optional: true + yaml: + optional: true + + '@voidzero-dev/vite-plus-darwin-arm64@1.1.0': + resolution: {integrity: sha512-iWJdQZBRZWB6X1Aadk0Rrs/NJeP32Qb0OGeWnu1AYL0g6ZCj1yy3l9Oa+HasDyG97s+4UYo523LdBAAv3dlFwg==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [darwin] - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + '@voidzero-dev/vite-plus-darwin-x64@1.1.0': + resolution: {integrity: sha512-IYSQ2KdigRMuVkKCC+e5uNliCTSHa8j3ajHsFG2cfXoiGHS1f4ausbMEzNYtn9Tcn0Xsyo7l0LN8lvB7MapJhA==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-PmrJG8Md5KCWi/akWJz5GRjei2QfRf48R4rPB9CfualzAAlpMOmaMGeV3eloWVAFye85uhdtzCmIAqN5OUVBJA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-zFr57++m5U474XEMcFGV025TOv9V2I5d87yylcuI/SvxQwDE9P7T5iRl55vGzQOzzInSnQuru6ByNIfmGa+84A==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-DXeRuTMylEbIVCQX+pSBM27lSPQwAKO6PDnGBJ6nFGhu37AmNeVl7cIChtcniXOuykbFduhSt3eIr/6CZHoEag==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-J7/Mb/kxtENUsL5Gh+8SSr/p054fSqU4oj0GEthr/2VEFSwZouOUigk6CzE7ANKFeTNJm40n8QCnjm7Oiy09pQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-EXWz8Tt7skmI0fLMSq5zYcuHo6SCF0reG55qDB/NMvWVIAca6Dborq7D1HyKA8v6p39zOl8r07gR7yXuYv7eeQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [win32] + + '@voidzero-dev/vite-plus-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-K9k77Mj/TszFHKsZePKsyfsIPo8HbjKeJUxaSJ6YZKAmSh9QvDsMFe4i4/HALwyii/PR1EQ8N0iN1nf9cNimGQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@yuku-codegen/binding-android-arm64@0.9.5': + resolution: {integrity: sha512-jrOY5WM+AaAqkv51fHP1x28ifto4WgcZVzodLUyMU1jMWn5Sq+VciRdk/n8E0Ey5w4p4cWWE+m5GfXWOYh7Kzw==} + cpu: [arm64] + os: [android] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-darwin-arm64@0.9.5': + resolution: {integrity: sha512-4O4lkCQIzPZGjiNB1GORSI6hBECbiiSBS/APZXPvNKYH+nhY4uuqv03LNXA+SET3hoBjvr95P5rIhY8KQaQUBA==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-darwin-x64@0.9.5': + resolution: {integrity: sha512-9EvoUO0SEhD6/d8VHdWuPerepPMSR1y84+UEgz8Un1Ope14Oe7xMvePpEQLTLovoIFZ8Zg3iZ8z8E11pZMqC3g==} + cpu: [x64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-freebsd-x64@0.9.5': + resolution: {integrity: sha512-HqT78WwgHTmp8lwujoUa9CrIortX4DdpuiVC18ZPSGvuuJf4ylpIEI6QrQEM78Zwz3muhAWAOXZ5irdiYY+AyA==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': + resolution: {integrity: sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm-musl@0.9.5': + resolution: {integrity: sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-win32-arm64@0.9.5': + resolution: {integrity: sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w==} + cpu: [arm64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-win32-x64@0.9.5': + resolution: {integrity: sha512-FxENahEjWSan59Syh/us/Kf4wNq8qrJLFJ3R2N4Oiwtb6yNdz/rWLNTIoNSssnsp7IoWJdUP6o/Z8ppg7lXMcg==} + cpu: [x64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-parser/binding-android-arm64@0.9.5': + resolution: {integrity: sha512-A2JCFCSHfnficqYEw4Iujpx7XrkMM3UfFcgJFmYpZSo9zM4nyhmdIIE0FogSduuU60lhM0/UcfuUBXIVNBMlGQ==} + cpu: [arm64] + os: [android] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-darwin-arm64@0.9.5': + resolution: {integrity: sha512-3PiyU+Eare4YuKaQ22N98/yAiROPY5o/NQJHraICzDvk4pgS+m+bgLKOvkGBRn33OnV95Vdv1Mn38b+MQHpULQ==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-darwin-x64@0.9.5': + resolution: {integrity: sha512-blFMAFI7AInI83XaiOF8cIeiRM46Nz9EfpZtZPRLbKxSA9aAr5v8aHYpmfN6NoyXxAv/10pwS4gsAuc1W6fy5Q==} + cpu: [x64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-freebsd-x64@0.9.5': + resolution: {integrity: sha512-TsNuL4qsZdO0tHp5GW43Y5fHeLPpPHA675wdcPNTcdq2ZO5AvsQWFFoiDg8CH4oBktfsQ9tdvKhjLnLYwKvp4g==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-gnu@0.9.5': + resolution: {integrity: sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-musl@0.9.5': + resolution: {integrity: sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + '@yuku-parser/binding-win32-arm64@0.9.5': + resolution: {integrity: sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg==} + cpu: [arm64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@yuku-parser/binding-win32-x64@0.9.5': + resolution: {integrity: sha512-FrERt9YWatY3bJfSUdi4YWY+6iQcyo3MzCC821BxlWM5TFZEHijnpz/bknR79VHlXY/9CvXz8ZlaAGPsTtN3nw==} + cpu: [x64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} + '@yuku-toolchain/types@0.9.5': + resolution: {integrity: sha512-KiuLNNgX9uNealaWAR+G3/cMXnRk9x4TY2EkYe/KIag+UPdwiA0RRf1hr1WAxzTP8KGzCTkqUdLPvqh32sEO3w==} acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} @@ -1060,11 +1203,12 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} @@ -1074,68 +1218,27 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} - engines: {node: '>=18'} - - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} - engines: {node: 20 || >=22} - braintrust@file:../../../js: resolution: {directory: ../../../js, type: directory} + engines: {node: '>=22.18.0'} hasBin: true peerDependencies: + '@types/react': '>=18' zod: ^3.25.34 || ^4.0 - - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} + peerDependenciesMeta: + '@types/react': + optional: true chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - cli-progress@3.12.0: - resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} - engines: {node: '>=4'} - - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1151,48 +1254,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1202,36 +1270,24 @@ packages: supports-color: optional: true - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -1239,15 +1295,6 @@ packages: es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} - es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -1257,9 +1304,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -1271,10 +1315,6 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - eventsource-parser@1.1.2: resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} engines: {node: '>=14.18'} @@ -1283,10 +1323,6 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1311,20 +1347,11 @@ packages: picomatch: optional: true - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1338,26 +1365,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graphql@16.14.0: resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - hasown@2.0.3: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} @@ -1365,24 +1376,9 @@ packages: headers-polyfill@5.0.1: resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} @@ -1394,12 +1390,8 @@ packages: is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -1478,60 +1470,30 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} meriyah@6.1.4: resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} engines: {node: '>=18.0.0'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1553,43 +1515,47 @@ packages: resolution: {integrity: sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.19: resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - obug@2.2.1: resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} + oxfmt@0.72.0: + resolution: {integrity: sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@7.0.2003: + resolution: {integrity: sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==} + hasBin: true + + oxlint@1.87.0: + resolution: {integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1597,9 +1563,6 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1614,62 +1577,24 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: ^8.5.18 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} postcss@8.5.28: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + protobufjs@7.6.5: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} - engines: {node: '>=0.6'} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -1683,10 +1608,6 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -1695,66 +1616,23 @@ packages: rettime@0.11.11: resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rollup@4.60.4: - resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semifies@1.0.0: resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - set-cookie-parser@3.1.0: resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1763,9 +1641,6 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -1784,11 +1659,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -1797,35 +1667,22 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - termi-link@1.1.0: - resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} - engines: {node: '>=12'} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} tinyexec@1.3.1: resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} engines: {node: '>=18'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@2.2.0: + resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.1: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} @@ -1837,66 +1694,26 @@ packages: resolution: {integrity: sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==} hasBin: true - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.5.18 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - type-fest@5.6.0: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} @@ -1908,67 +1725,36 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vite@8.0.14: - resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} - engines: {node: ^20.19.0 || >=22.12.0} + vite-plus@1.1.0: + resolution: {integrity: sha512-khkRlY70qqbBV0QmF9QKXSrhxIRYftb1H05AJMowMoq7iouchmAoXYeReZ8pKKMOadqrWVxFa3S9BA2m7w59iA==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + playwright: '*' peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: + '@vitest/browser-playwright': optional: true - tsx: + '@vitest/browser-webdriverio': optional: true - yaml: + playwright: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 happy-dom: '*' jsdom: '*' vite: ^8.0.5 @@ -1999,17 +1785,26 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} hasBin: true wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + 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 y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} @@ -2023,6 +1818,15 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yuku-ast@0.9.5: + resolution: {integrity: sha512-Q8qW8WwQnN5Cm0ZZivdRIfv0sRLTjUq0YumXJkw8CYN1aCdICH9rk4C47/4n6kA5EqDtlj+F608twoTSV2MwdQ==} + + yuku-codegen@0.9.5: + resolution: {integrity: sha512-zGUVyDpSK4b6c9B0yyxi2P0wujn1XZTbG9dCb7d6gyAoP63EMgyLzUwPUvwxPG5jgYqhlI7w+S3oCyapqGr4PA==} + + yuku-parser@0.9.5: + resolution: {integrity: sha512-IBnAdNVMswWbJcM7woPluOudectJzFjJ1N8jVRxP9Uq4CIv5hZdBIcdmtRbFDYF/Ph2j2gVup4YJ4N9mh0jYFA==} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -2033,26 +1837,17 @@ packages: snapshots: - '@braintrust/bt-darwin-arm64@0.12.0': - optional: true - - '@braintrust/bt-darwin-x64@0.12.0': - optional: true - - '@braintrust/bt-linux-arm64@0.12.0': - optional: true - - '@braintrust/bt-linux-x64-musl@0.12.0': - optional: true + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 - '@braintrust/bt-linux-x64@0.12.0': - optional: true + '@babel/helper-validator-identifier@7.29.7': {} - '@braintrust/bt-win32-arm64@0.12.0': - optional: true + '@babel/runtime@7.29.10': {} - '@braintrust/bt-win32-x64@0.12.0': - optional: true + '@blazediff/core@1.10.0': {} '@braintrust/otel@file:..(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.0.0(@opentelemetry/api@1.9.0))(@opentelemetry/exporter-trace-otlp-http@0.200.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.0(@opentelemetry/api@1.9.0))(braintrust@file:../../../js(zod@4.4.3))': dependencies: @@ -2062,178 +1857,81 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) braintrust: file:../../../js(zod@4.4.3) - '@colors/colors@1.5.0': - optional: true - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.27.7': - optional: true - '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.7': - optional: true - '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.7': - optional: true - '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.7': - optional: true - '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.7': - optional: true - '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.7': - optional: true - '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.7': - optional: true - '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.7': - optional: true - '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.7': - optional: true - '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.7': - optional: true - '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.7': - optional: true - '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.7': - optional: true - '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.7': - optional: true - '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.7': - optional: true - '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.7': - optional: true - '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.7': - optional: true - '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.7': - optional: true - '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.7': - optional: true - '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.7': - optional: true - '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': - optional: true - '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.7': - optional: true - '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.7': - optional: true - '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.7': - optional: true - '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.7': - optional: true - '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.7': - optional: true - '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.7': - optional: true - '@esbuild/win32-x64@0.28.1': optional: true @@ -2290,6 +1988,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -2306,15 +2006,6 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.4 - optional: true - - '@next/env@14.2.35': {} - '@open-draft/deferred-promise@2.2.0': {} '@open-draft/deferred-promise@3.0.0': {} @@ -2559,160 +2250,182 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} - '@oxc-project/types@0.132.0': {} - - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.1': {} - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 + '@oxc-project/runtime@0.153.0': {} - '@protobufjs/float@1.0.2': {} + '@oxc-project/types@0.153.0': {} - '@protobufjs/path@1.1.2': {} + '@oxfmt/binding-android-arm-eabi@0.72.0': + optional: true - '@protobufjs/pool@1.1.0': {} + '@oxfmt/binding-android-arm64@0.72.0': + optional: true - '@protobufjs/utf8@1.1.1': {} + '@oxfmt/binding-darwin-arm64@0.72.0': + optional: true - '@rolldown/binding-android-arm64@1.0.2': + '@oxfmt/binding-darwin-x64@0.72.0': optional: true - '@rolldown/binding-darwin-arm64@1.0.2': + '@oxfmt/binding-freebsd-x64@0.72.0': optional: true - '@rolldown/binding-darwin-x64@1.0.2': + '@oxfmt/binding-linux-arm-gnueabihf@0.72.0': optional: true - '@rolldown/binding-freebsd-x64@1.0.2': + '@oxfmt/binding-linux-arm-musleabihf@0.72.0': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + '@oxfmt/binding-linux-arm64-gnu@0.72.0': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': + '@oxfmt/binding-linux-arm64-musl@0.72.0': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': + '@oxfmt/binding-linux-ppc64-gnu@0.72.0': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': + '@oxfmt/binding-linux-riscv64-gnu@0.72.0': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': + '@oxfmt/binding-linux-riscv64-musl@0.72.0': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': + '@oxfmt/binding-linux-s390x-gnu@0.72.0': optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': + '@oxfmt/binding-linux-x64-gnu@0.72.0': optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': + '@oxfmt/binding-linux-x64-musl@0.72.0': optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@oxfmt/binding-openharmony-arm64@0.72.0': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': + '@oxfmt/binding-win32-arm64-msvc@0.72.0': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': + '@oxfmt/binding-win32-ia32-msvc@0.72.0': optional: true - '@rolldown/pluginutils@1.0.1': {} + '@oxfmt/binding-win32-x64-msvc@0.72.0': + optional: true - '@rollup/rollup-android-arm-eabi@4.60.4': + '@oxlint-tsgolint/darwin-arm64@7.0.2003': optional: true - '@rollup/rollup-android-arm64@4.60.4': + '@oxlint-tsgolint/darwin-x64@7.0.2003': optional: true - '@rollup/rollup-darwin-arm64@4.60.4': + '@oxlint-tsgolint/linux-arm64@7.0.2003': optional: true - '@rollup/rollup-darwin-x64@4.60.4': + '@oxlint-tsgolint/linux-x64@7.0.2003': optional: true - '@rollup/rollup-freebsd-arm64@4.60.4': + '@oxlint-tsgolint/win32-arm64@7.0.2003': optional: true - '@rollup/rollup-freebsd-x64@4.60.4': + '@oxlint-tsgolint/win32-x64@7.0.2003': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + '@oxlint/binding-android-arm-eabi@1.87.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.4': + '@oxlint/binding-android-arm64@1.87.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.4': + '@oxlint/binding-darwin-arm64@1.87.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.4': + '@oxlint/binding-darwin-x64@1.87.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.4': + '@oxlint/binding-freebsd-x64@1.87.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.4': + '@oxlint/binding-linux-arm-gnueabihf@1.87.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.4': + '@oxlint/binding-linux-arm-musleabihf@1.87.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.4': + '@oxlint/binding-linux-arm64-gnu@1.87.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.4': + '@oxlint/binding-linux-arm64-musl@1.87.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.4': + '@oxlint/binding-linux-ppc64-gnu@1.87.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.4': + '@oxlint/binding-linux-riscv64-gnu@1.87.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.4': + '@oxlint/binding-linux-riscv64-musl@1.87.0': optional: true - '@rollup/rollup-linux-x64-musl@4.60.4': + '@oxlint/binding-linux-s390x-gnu@1.87.0': optional: true - '@rollup/rollup-openbsd-x64@4.60.4': + '@oxlint/binding-linux-x64-gnu@1.87.0': optional: true - '@rollup/rollup-openharmony-arm64@4.60.4': + '@oxlint/binding-linux-x64-musl@1.87.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.4': + '@oxlint/binding-openharmony-arm64@1.87.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.4': + '@oxlint/binding-win32-arm64-msvc@1.87.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.4': + '@oxlint/binding-win32-ia32-msvc@1.87.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.4': + '@oxlint/binding-win32-x64-msvc@1.87.0': optional: true - '@standard-schema/spec@1.1.0': {} + '@oxlint/plugins@1.79.0': {} + + '@polka/url@1.0.0-next.29': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} - '@tybys/wasm-util@0.10.4': + '@protobufjs/fetch@1.1.1': dependencies: - tslib: 2.8.1 - optional: true + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.10 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/user-event@14.6.8(@testing-library/dom@10.4.2)': + dependencies: + '@testing-library/dom': 10.4.2 + + '@types/aria-query@5.0.4': {} '@types/chai@5.2.3': dependencies: @@ -2721,8 +2434,6 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/estree@1.0.8': {} - '@types/estree@1.0.9': {} '@types/node@22.19.19': @@ -2739,52 +2450,194 @@ snapshots: '@vercel/functions@1.6.0': {} - '@vitest/expect@4.1.11': + '@vitest/browser-preview@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3)': dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.8(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + ws: 8.22.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite - '@vitest/mocker@4.1.11(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7))': + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))': dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.3 optionalDependencies: msw: 2.14.6(@types/node@22.19.19)(typescript@5.5.4) - vite: 8.0.14(@types/node@22.19.19)(esbuild@0.27.7) + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)' + + '@vitest/pretty-format@5.0.3': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/snapshot@5.0.3': + dependencies: + '@vitest/pretty-format': 5.0.3 + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pathe: 2.0.3 + + '@vitest/spy@5.0.3': {} + + '@vitest/ui@5.0.3(vitest@5.0.3)': + dependencies: + '@vitest/utils': 5.0.3 + fflate: 0.8.3 + flatted: 3.4.4 + pathe: 2.0.3 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + + '@vitest/utils@5.0.3': + dependencies: + '@vitest/pretty-format': 5.0.3 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)': + dependencies: + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.28 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@types/node': 22.19.19 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + typescript: 5.5.4 + + '@voidzero-dev/vite-plus-darwin-arm64@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-darwin-x64@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-musl@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-gnu@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-win32-x64-msvc@1.1.0': + optional: true + + '@yuku-codegen/binding-android-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.9.5': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-win32-x64@0.9.5': + optional: true + + '@yuku-parser/binding-android-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-darwin-x64@0.9.5': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.9.5': + optional: true - '@vitest/pretty-format@4.1.11': - dependencies: - tinyrainbow: 3.1.1 + '@yuku-parser/binding-linux-arm-musl@0.9.5': + optional: true - '@vitest/runner@4.1.11': - dependencies: - '@vitest/utils': 4.1.11 - pathe: 2.0.3 + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + optional: true - '@vitest/snapshot@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pathe: 2.0.3 + '@yuku-parser/binding-linux-arm64-musl@0.9.5': + optional: true - '@vitest/spy@4.1.11': {} + '@yuku-parser/binding-linux-x64-gnu@0.9.5': + optional: true - '@vitest/utils@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 + '@yuku-parser/binding-linux-x64-musl@0.9.5': + optional: true - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 + '@yuku-parser/binding-win32-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-win32-x64@0.9.5': + optional: true + + '@yuku-toolchain/types@0.9.5': {} acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: @@ -2805,116 +2658,45 @@ snapshots: dependencies: color-convert: 2.0.1 - any-promise@1.3.0: {} + ansi-styles@5.2.0: {} - argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 assertion-error@2.0.1: {} astring@1.9.0: {} - balanced-match@4.0.4: {} - - body-parser@2.3.0: - dependencies: - bytes: 3.1.2 - content-type: 2.0.0 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.15.2 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - - brace-expansion@5.0.9: - dependencies: - balanced-match: 4.0.4 - braintrust@file:../../../js(zod@4.4.3): dependencies: - '@next/env': 14.2.35 '@vercel/functions': 1.6.0 acorn: 8.16.0 acorn-import-attributes: 1.9.5(acorn@8.16.0) ajv: 8.20.0 - argparse: 2.0.1 astring: 1.9.0 cjs-module-lexer: 2.2.0 - cli-progress: 3.12.0 - cli-table3: 0.6.5 - cors: 2.8.6 dotenv: 16.6.1 - esbuild: 0.28.1 esquery: 1.7.0 eventsource-parser: 1.1.2 - express: 5.2.1 - http-errors: 2.0.1 meriyah: 6.1.4 - minimatch: 10.2.5 module-details-from-path: 1.0.4 mustache: 4.2.0 - pluralize: 8.0.0 semifies: 1.0.0 source-map: 0.7.6 - termi-link: 1.1.0 unplugin: 2.3.11 uuid: 11.1.1 zod: 4.4.3 zod-to-json-schema: 3.25.2(zod@4.4.3) - optionalDependencies: - '@braintrust/bt-darwin-arm64': 0.12.0 - '@braintrust/bt-darwin-x64': 0.12.0 - '@braintrust/bt-linux-arm64': 0.12.0 - '@braintrust/bt-linux-x64': 0.12.0 - '@braintrust/bt-linux-x64-musl': 0.12.0 - '@braintrust/bt-win32-arm64': 0.12.0 - '@braintrust/bt-win32-x64': 0.12.0 transitivePeerDependencies: - '@aws-sdk/credential-provider-web-identity' - - supports-color - - bundle-require@5.1.0(esbuild@0.27.7): - dependencies: - esbuild: 0.27.7 - load-tsconfig: 0.2.5 - - bytes@3.1.2: {} - - cac@6.7.14: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 chai@6.2.2: {} - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - cjs-module-lexer@1.4.3: {} cjs-module-lexer@2.2.0: {} - cli-progress@3.12.0: - dependencies: - string-width: 4.2.3 - - cli-table3@0.6.5: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - cli-width@4.1.0: {} cliui@8.0.1: @@ -2929,92 +2711,28 @@ snapshots: color-name@1.1.4: {} - commander@4.1.1: {} - - confbox@0.1.8: {} - - consola@3.4.2: {} - - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - - content-type@2.0.0: {} - convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - cookie@1.1.1: {} - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - debug@4.4.3: dependencies: ms: 2.1.3 - depd@2.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} - dotenv@16.6.1: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 + dom-accessibility-api@0.5.16: {} - ee-first@1.1.1: {} + dotenv@16.6.1: {} emoji-regex@8.0.0: {} - encodeurl@2.0.0: {} - - es-define-property@1.0.1: {} - es-errors@1.3.0: {} es-module-lexer@2.3.2: {} - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -3043,11 +2761,10 @@ snapshots: '@esbuild/win32-arm64': 0.28.1 '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + optional: true escalade@3.2.0: {} - escape-html@1.0.3: {} - esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -3058,45 +2775,10 @@ snapshots: dependencies: '@types/estree': 1.0.9 - etag@1.8.1: {} - eventsource-parser@1.1.2: {} expect-type@1.4.0: {} - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.3.0 - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.2 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - fast-deep-equal@3.1.3: {} fast-string-truncated-width@3.0.3: {} @@ -3111,34 +2793,13 @@ snapshots: dependencies: fast-string-width: 3.0.2 - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.2 - rollup: 4.60.4 - - forwarded@0.2.0: {} + fflate@0.8.3: {} - fresh@2.0.0: {} + flatted@3.4.4: {} fsevents@2.3.3: optional: true @@ -3147,30 +2808,8 @@ snapshots: get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.3 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 - - gopd@1.2.0: {} - graphql@16.14.0: {} - has-symbols@1.1.0: {} - hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -3180,18 +2819,6 @@ snapshots: '@types/set-cookie-parser': 2.4.10 set-cookie-parser: 3.1.0 - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - import-in-the-middle@1.15.0: dependencies: acorn: 8.16.0 @@ -3199,10 +2826,6 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 - inherits@2.0.4: {} - - ipaddr.js@1.9.1: {} - is-core-module@2.16.2: dependencies: hasown: 2.0.3 @@ -3211,9 +2834,7 @@ snapshots: is-node-process@1.2.0: {} - is-promise@4.0.0: {} - - joycon@3.1.1: {} + js-tokens@4.0.0: {} json-schema-traverse@1.0.0: {} @@ -3266,47 +2887,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - lodash.camelcase@4.3.0: {} long@5.3.2: {} - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - math-intrinsics@1.1.0: {} - - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - - meriyah@6.1.4: {} - - mime-db@1.54.0: {} + lz-string@1.5.0: {} - mime-types@3.0.2: + magic-string@1.4.3: dependencies: - mime-db: 1.54.0 + '@jridgewell/sourcemap-codec': 1.6.0 - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.9 - - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.4 + meriyah@6.1.4: {} module-details-from-path@1.0.4: {} + mrmime@2.0.1: {} + ms@2.1.3: {} msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4): @@ -3338,40 +2934,74 @@ snapshots: mute-stream@4.0.0: {} - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - nanoid@3.3.19: {} - negotiator@1.0.0: {} - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - obug@2.2.1: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - outvariant@1.4.3: {} - parseurl@1.3.3: {} + oxfmt@0.72.0(vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4)): + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4) + + oxlint-tsgolint@7.0.2003: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 7.0.2003 + '@oxlint-tsgolint/darwin-x64': 7.0.2003 + '@oxlint-tsgolint/linux-arm64': 7.0.2003 + '@oxlint-tsgolint/linux-x64': 7.0.2003 + '@oxlint-tsgolint/win32-arm64': 7.0.2003 + '@oxlint-tsgolint/win32-x64': 7.0.2003 + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4) path-parse@1.0.7: {} path-to-regexp@6.3.0: {} - path-to-regexp@8.4.2: {} - pathe@2.0.3: {} picocolors@1.1.1: {} @@ -3380,21 +3010,7 @@ snapshots: picomatch@4.0.7: {} - pirates@4.0.7: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - pluralize@8.0.0: {} - - postcss-load-config@6.0.1(postcss@8.5.28): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - postcss: 8.5.28 + pngjs@7.0.0: {} postcss@8.5.28: dependencies: @@ -3402,6 +3018,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + protobufjs@7.6.5: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -3416,25 +3038,7 @@ snapshots: '@types/node': 22.19.19 long: 5.3.2 - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - qs@6.15.2: - dependencies: - side-channel: 1.1.0 - - range-parser@1.2.1: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - - readdirp@4.1.2: {} + react-is@17.0.2: {} require-directory@2.1.1: {} @@ -3448,8 +3052,6 @@ snapshots: transitivePeerDependencies: - supports-color - resolve-from@5.0.0: {} - resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -3459,141 +3061,24 @@ snapshots: rettime@0.11.11: {} - rolldown@1.0.2: - dependencies: - '@oxc-project/types': 0.132.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 - - rollup@4.60.4: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.4 - '@rollup/rollup-android-arm64': 4.60.4 - '@rollup/rollup-darwin-arm64': 4.60.4 - '@rollup/rollup-darwin-x64': 4.60.4 - '@rollup/rollup-freebsd-arm64': 4.60.4 - '@rollup/rollup-freebsd-x64': 4.60.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 - '@rollup/rollup-linux-arm-musleabihf': 4.60.4 - '@rollup/rollup-linux-arm64-gnu': 4.60.4 - '@rollup/rollup-linux-arm64-musl': 4.60.4 - '@rollup/rollup-linux-loong64-gnu': 4.60.4 - '@rollup/rollup-linux-loong64-musl': 4.60.4 - '@rollup/rollup-linux-ppc64-gnu': 4.60.4 - '@rollup/rollup-linux-ppc64-musl': 4.60.4 - '@rollup/rollup-linux-riscv64-gnu': 4.60.4 - '@rollup/rollup-linux-riscv64-musl': 4.60.4 - '@rollup/rollup-linux-s390x-gnu': 4.60.4 - '@rollup/rollup-linux-x64-gnu': 4.60.4 - '@rollup/rollup-linux-x64-musl': 4.60.4 - '@rollup/rollup-openbsd-x64': 4.60.4 - '@rollup/rollup-openharmony-arm64': 4.60.4 - '@rollup/rollup-win32-arm64-msvc': 4.60.4 - '@rollup/rollup-win32-ia32-msvc': 4.60.4 - '@rollup/rollup-win32-x64-gnu': 4.60.4 - '@rollup/rollup-win32-x64-msvc': 4.60.4 - fsevents: 2.3.3 - - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - - safer-buffer@2.1.2: {} - semifies@1.0.0: {} - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - set-cookie-parser@3.1.0: {} - setprototypeof@1.2.0: {} - shimmer@1.2.1: {} - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 + signal-exit@4.1.0: {} - side-channel@1.1.0: + sirv@3.0.2: dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - siginfo@2.0.0: {} - - signal-exit@4.1.0: {} + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 source-map-js@1.2.1: {} source-map@0.7.6: {} - stackback@0.0.2: {} - statuses@2.0.2: {} std-env@4.2.0: {} @@ -3610,46 +3095,21 @@ snapshots: dependencies: ansi-regex: 5.0.1 - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.16 - ts-interface-checker: 0.1.13 - supports-preserve-symlinks-flag@1.0.0: {} tagged-tag@1.0.0: {} - termi-link@1.1.0: {} - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} + tinybench@6.2.2: {} tinyexec@1.3.1: {} - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinypool@2.2.0: {} + tinyrainbow@3.1.1: {} tldts-core@7.4.0: {} @@ -3658,65 +3118,20 @@ snapshots: dependencies: tldts-core: 7.4.0 - toidentifier@1.0.1: {} + totalist@3.0.1: {} tough-cookie@6.0.1: dependencies: tldts: 7.4.0 - tree-kill@1.2.2: {} - - ts-interface-checker@0.1.13: {} - - tslib@2.8.1: - optional: true - - tsup@8.5.1(postcss@8.5.28)(typescript@5.5.4): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.7) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.7 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.28) - resolve-from: 5.0.0 - rollup: 4.60.4 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.16 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.28 - typescript: 5.5.4 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - type-fest@5.6.0: dependencies: tagged-tag: 1.0.0 - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - typescript@5.5.4: {} - ufo@1.6.4: {} - undici-types@6.21.0: {} - unpipe@1.0.0: {} - unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -3728,54 +3143,92 @@ snapshots: uuid@11.1.1: {} - vary@1.1.2: {} - - vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.7 - postcss: 8.5.28 - rolldown: 1.0.2 - tinyglobby: 0.2.17 + vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4): + dependencies: + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(typescript@5.5.4)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)' + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) optionalDependencies: - '@types/node': 22.19.19 - esbuild: 0.27.7 - fsevents: 2.3.3 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc + - yaml - vitest@4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)): + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4)) + chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.3 obug: 2.2.1 - pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 + tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.0.14(@types/node@22.19.19)(esbuild@0.27.7) - why-is-node-running: 2.3.0 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4)' + why-is-node-running: 3.2.1 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.19 + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@22.19.19)(esbuild@0.28.1)(typescript@5.5.4))(msw@2.14.6(@types/node@22.19.19)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) transitivePeerDependencies: - msw webpack-virtual-modules@0.6.2: {} - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 + why-is-node-running@3.2.1: {} wrap-ansi@7.0.0: dependencies: @@ -3783,7 +3236,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} + ws@8.22.0: {} y18n@5.0.8: {} @@ -3799,6 +3252,45 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yuku-ast@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + + yuku-codegen@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.9.5 + '@yuku-codegen/binding-darwin-arm64': 0.9.5 + '@yuku-codegen/binding-darwin-x64': 0.9.5 + '@yuku-codegen/binding-freebsd-x64': 0.9.5 + '@yuku-codegen/binding-linux-arm-gnu': 0.9.5 + '@yuku-codegen/binding-linux-arm-musl': 0.9.5 + '@yuku-codegen/binding-linux-arm64-gnu': 0.9.5 + '@yuku-codegen/binding-linux-arm64-musl': 0.9.5 + '@yuku-codegen/binding-linux-x64-gnu': 0.9.5 + '@yuku-codegen/binding-linux-x64-musl': 0.9.5 + '@yuku-codegen/binding-win32-arm64': 0.9.5 + '@yuku-codegen/binding-win32-x64': 0.9.5 + + yuku-parser@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + yuku-ast: 0.9.5 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.9.5 + '@yuku-parser/binding-darwin-arm64': 0.9.5 + '@yuku-parser/binding-darwin-x64': 0.9.5 + '@yuku-parser/binding-freebsd-x64': 0.9.5 + '@yuku-parser/binding-linux-arm-gnu': 0.9.5 + '@yuku-parser/binding-linux-arm-musl': 0.9.5 + '@yuku-parser/binding-linux-arm64-gnu': 0.9.5 + '@yuku-parser/binding-linux-arm64-musl': 0.9.5 + '@yuku-parser/binding-linux-x64-gnu': 0.9.5 + '@yuku-parser/binding-linux-x64-musl': 0.9.5 + '@yuku-parser/binding-win32-arm64': 0.9.5 + '@yuku-parser/binding-win32-x64': 0.9.5 + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/integrations/otel-js/otel-v2/tests/otel-v2.test.ts b/integrations/otel-js/otel-v2/tests/otel-v2.test.ts index e9fd88494..988188dee 100644 --- a/integrations/otel-js/otel-v2/tests/otel-v2.test.ts +++ b/integrations/otel-js/otel-v2/tests/otel-v2.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/consistent-type-assertions */ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi } from "vite-plus/test"; import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base"; import { BraintrustExporter } from "../src/otel"; diff --git a/integrations/otel-js/otel-v2/vitest.config.ts b/integrations/otel-js/otel-v2/vite.config.ts similarity index 93% rename from integrations/otel-js/otel-v2/vitest.config.ts rename to integrations/otel-js/otel-v2/vite.config.ts index 6c805c533..3d8be82c0 100644 --- a/integrations/otel-js/otel-v2/vitest.config.ts +++ b/integrations/otel-js/otel-v2/vite.config.ts @@ -1,5 +1,5 @@ import { readFileSync } from "node:fs"; -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vite-plus"; import { detectOtelVersion, logOtelVersions, diff --git a/integrations/otel-js/package.json b/integrations/otel-js/package.json index 38aee93b6..48b3e97d8 100644 --- a/integrations/otel-js/package.json +++ b/integrations/otel-js/package.json @@ -3,21 +3,19 @@ "version": "1.0.2", "description": "SDK for integrating Braintrust with OpenTelemetry", "main": "./dist/index.js", - "module": "./dist/index.mjs", + "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { "./package.json": "./package.json", ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" + "default": "./dist/index.js" } }, "scripts": { - "build": "tsup", + "build": "vp run compile", "check:typings": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", - "watch": "tsup --watch", + "watch": "vp pack --watch", "clean": "rm -r dist/*", "test": "pnpm test:v1 && pnpm test:v2", "test:v1": "cd otel-v1 && pnpm test", @@ -30,9 +28,8 @@ "@opentelemetry/context-async-hooks": "2.6.1", "@types/node": "^22.15.21", "braintrust": "workspace:*", - "tsup": "^8.5.0", "typescript": "5.5.4", - "vitest": "4.1.11" + "vite-plus": "catalog:" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0", @@ -68,5 +65,10 @@ "access": "public", "registry": "https://registry.npmjs.org/", "provenance": true - } + }, + "type": "module", + "engines": { + "node": ">=22.18.0" + }, + "sideEffects": false } diff --git a/integrations/otel-js/run-tests.sh b/integrations/otel-js/run-tests.sh index c3a7b4d0f..59b97c916 100644 --- a/integrations/otel-js/run-tests.sh +++ b/integrations/otel-js/run-tests.sh @@ -2,4 +2,4 @@ set -e pnpm install --ignore-workspace -pnpm exec vitest run +pnpm exec vp test run diff --git a/integrations/otel-js/smoke/scenarios/otel-v1/Makefile b/integrations/otel-js/smoke/scenarios/otel-v1/Makefile index 7654613f1..f5c7f8159 100644 --- a/integrations/otel-js/smoke/scenarios/otel-v1/Makefile +++ b/integrations/otel-js/smoke/scenarios/otel-v1/Makefile @@ -13,7 +13,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-latest.tgz" ] && \ @@ -35,7 +35,7 @@ setup: echo "==> Using BRAINTRUST_OTEL_TAR: $(BRAINTRUST_OTEL_TAR)"; \ else \ echo "==> Building @braintrust/otel package"; \ - cd ../../../../../integrations/otel-js && pnpm exec turbo build --filter=@braintrust/otel && pnpm pack --pack-destination ../../js/artifacts; \ + cd ../../../../../integrations/otel-js && pnpm --filter=@braintrust/otel run build && pnpm pack --pack-destination ../../js/artifacts; \ \ for f in ../../../js/artifacts/braintrust-otel-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-otel-latest.tgz" ]; then \ diff --git a/integrations/otel-js/smoke/scenarios/otel-v1/tests/basic.test.ts b/integrations/otel-js/smoke/scenarios/otel-v1/tests/basic.test.ts index 1cdb05f89..d7a307a04 100644 --- a/integrations/otel-js/smoke/scenarios/otel-v1/tests/basic.test.ts +++ b/integrations/otel-js/smoke/scenarios/otel-v1/tests/basic.test.ts @@ -8,7 +8,7 @@ import { displayTestResults, hasFailures, type TestResult, -} from "../../../../../../js/smoke/shared/dist/index.mjs"; +} from "../../../../../../js/smoke/shared/dist/index.js"; async function main() { const results: TestResult[] = []; diff --git a/integrations/otel-js/smoke/scenarios/otel-v1/tests/filtering.test.ts b/integrations/otel-js/smoke/scenarios/otel-v1/tests/filtering.test.ts index e460a81a7..bd0362f91 100644 --- a/integrations/otel-js/smoke/scenarios/otel-v1/tests/filtering.test.ts +++ b/integrations/otel-js/smoke/scenarios/otel-v1/tests/filtering.test.ts @@ -8,7 +8,7 @@ import { displayTestResults, hasFailures, type TestResult, -} from "../../../../../../js/smoke/shared/dist/index.mjs"; +} from "../../../../../../js/smoke/shared/dist/index.js"; type OtelPayload = { resourceSpans?: Array<{ diff --git a/integrations/otel-js/src/exporter.test.ts b/integrations/otel-js/src/exporter.test.ts index b0391222d..34040036c 100644 --- a/integrations/otel-js/src/exporter.test.ts +++ b/integrations/otel-js/src/exporter.test.ts @@ -7,7 +7,7 @@ import { beforeEach, afterEach, vi, -} from "vitest"; +} from "vite-plus/test"; import { createServer, type Server } from "node:http"; import type { AddressInfo } from "node:net"; import { diff --git a/integrations/otel-js/src/id-gen.test.ts b/integrations/otel-js/src/id-gen.test.ts index 19fcb6f93..37171966d 100644 --- a/integrations/otel-js/src/id-gen.test.ts +++ b/integrations/otel-js/src/id-gen.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe, beforeEach, afterEach } from "vitest"; +import { expect, test, describe, beforeEach, afterEach } from "vite-plus/test"; import { OTELIDGenerator, UUIDGenerator, diff --git a/integrations/otel-js/src/index.ts b/integrations/otel-js/src/index.ts index e366947be..564e2aebe 100644 --- a/integrations/otel-js/src/index.ts +++ b/integrations/otel-js/src/index.ts @@ -11,7 +11,7 @@ export { export { BraintrustSpanProcessor, BraintrustExporter } from "./otel"; -export const setupOtelCompat = () => { +export const setupOtelCompat = (): void => { // Read braintrust's shared registration directly so this works with any // supported braintrust version. Keep the message in sync with braintrust. const spanCustomizers: unknown = (globalThis as Record)[ @@ -26,6 +26,6 @@ export const setupOtelCompat = () => { configureContextManager(OtelContextManager); }; -export const resetOtelCompat = () => { +export const resetOtelCompat = (): void => { configureContextManager(undefined); }; diff --git a/integrations/otel-js/src/otel-compat.test.ts b/integrations/otel-js/src/otel-compat.test.ts index 35db904c6..697af5950 100644 --- a/integrations/otel-js/src/otel-compat.test.ts +++ b/integrations/otel-js/src/otel-compat.test.ts @@ -5,7 +5,7 @@ * when created in mixed contexts. */ -import { beforeEach, afterEach, describe, expect, test } from "vitest"; +import { beforeEach, afterEach, describe, expect, test } from "vite-plus/test"; import { initLogger, currentSpan, Eval } from "braintrust"; import { _exportsForTestingOnly, diff --git a/integrations/otel-js/src/otel.test.ts b/integrations/otel-js/src/otel.test.ts index 462633426..68cec3830 100644 --- a/integrations/otel-js/src/otel.test.ts +++ b/integrations/otel-js/src/otel.test.ts @@ -1,7 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/consistent-type-assertions */ import { readFileSync } from "node:fs"; -import { describe, it, expect, beforeEach, afterEach, vi, test } from "vitest"; +import { + describe, + it, + expect, + beforeEach, + afterEach, + vi, + test, +} from "vite-plus/test"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; diff --git a/integrations/otel-js/src/span-customizers.test.ts b/integrations/otel-js/src/span-customizers.test.ts index be375653e..27ad082b4 100644 --- a/integrations/otel-js/src/span-customizers.test.ts +++ b/integrations/otel-js/src/span-customizers.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test, vi } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vite-plus/test"; import { BasicTracerProvider, InMemorySpanExporter, diff --git a/integrations/otel-js/tests/utils.ts b/integrations/otel-js/tests/utils.ts index a435f7515..f7eb32b4e 100644 --- a/integrations/otel-js/tests/utils.ts +++ b/integrations/otel-js/tests/utils.ts @@ -144,6 +144,7 @@ export function createOtelAliases(cwd: string): Record { ? path.resolve(cwd, "../..") : path.resolve(cwd, "../../.."); return { + "#braintrust-platform": path.resolve(repositoryRoot, "js/src/isomorph.ts"), "@braintrust/otel": path.resolve( repositoryRoot, "integrations/otel-js/src/index.ts", diff --git a/integrations/otel-js/tsconfig.test.json b/integrations/otel-js/tsconfig.test.json index ad0f1bdc1..ce7070daf 100644 --- a/integrations/otel-js/tsconfig.test.json +++ b/integrations/otel-js/tsconfig.test.json @@ -7,6 +7,7 @@ "baseUrl": ".", "paths": { "@braintrust/otel": ["src/index.ts"], + "#braintrust-platform": ["../../js/src/isomorph.ts"], "braintrust": ["../../js/src/node/index.ts"], "braintrust/instrumentation": ["../../js/src/instrumentation/index.ts"] } diff --git a/integrations/otel-js/tsup.config.ts b/integrations/otel-js/tsup.config.ts deleted file mode 100644 index 05319102e..000000000 --- a/integrations/otel-js/tsup.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from "tsup"; -import { readFileSync } from "node:fs"; - -const packageJson = JSON.parse( - readFileSync(new URL("./package.json", import.meta.url), "utf8"), -) as { version: string }; - -export default defineConfig([ - { - entry: ["src/index.ts"], - format: ["cjs", "esm"], - outDir: "dist", - external: [ - "braintrust", - "@opentelemetry/api", - "@opentelemetry/core", - "@opentelemetry/exporter-trace-otlp-http", - "@opentelemetry/sdk-trace-base", - "@opentelemetry/context-async-hooks", - ], - define: { - __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), - }, - dts: true, - }, -]); diff --git a/integrations/otel-js/vite.config.ts b/integrations/otel-js/vite.config.ts new file mode 100644 index 000000000..0789d1703 --- /dev/null +++ b/integrations/otel-js/vite.config.ts @@ -0,0 +1,39 @@ +import { defineConfig } from "vite-plus"; +import { readFileSync } from "node:fs"; +import { createOtelAliases } from "./tests/utils"; + +const packageJson = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +) as { version: string }; + +export default defineConfig({ + define: { __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version) }, + resolve: { alias: createOtelAliases(process.cwd()) }, + test: { + reporters: ["default"], + include: ["tests/**/*.test.ts", "src/**/*.test.ts"], + }, + run: { + tasks: { + compile: { + command: "vp pack", + dependsOn: [ + { task: "compile", from: ["dependencies", "devDependencies"] }, + ], + cache: { + input: [{ auto: true }, "!dist/**"], + untrackedEnv: ["SHELL", "BASH_ENV"], + output: ["dist/**"], + }, + }, + }, + }, + pack: { + define: { + __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), + }, + dts: { generator: "oxc" }, + sourcemap: true, + fixedExtension: false, + }, +}); diff --git a/integrations/otel-js/vitest.config.ts b/integrations/otel-js/vitest.config.ts deleted file mode 100644 index bf4fb3a75..000000000 --- a/integrations/otel-js/vitest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "vitest/config"; -import { readFileSync } from "node:fs"; -import { createOtelAliases } from "./tests/utils"; - -const packageJson = JSON.parse( - readFileSync(new URL("./package.json", import.meta.url), "utf8"), -) as { version: string }; - -export default defineConfig({ - define: { - __BRAINTRUST_OTEL_VERSION__: JSON.stringify(packageJson.version), - }, - resolve: { alias: createOtelAliases(process.cwd()) }, - test: { - reporters: ["default"], - include: ["tests/**/*.test.ts", "src/**/*.test.ts"], - }, -}); diff --git a/integrations/templates-nunjucks/.gitignore b/integrations/templates-nunjucks/.gitignore new file mode 100644 index 000000000..7a6c09645 --- /dev/null +++ b/integrations/templates-nunjucks/.gitignore @@ -0,0 +1 @@ +.vitest/ diff --git a/integrations/templates-nunjucks/package.json b/integrations/templates-nunjucks/package.json index 794c10e20..25db0b0e9 100644 --- a/integrations/templates-nunjucks/package.json +++ b/integrations/templates-nunjucks/package.json @@ -3,26 +3,24 @@ "version": "1.0.0", "description": "Nunjucks templating support for the Braintrust JS SDK", "main": "./dist/index.js", - "module": "./dist/index.mjs", + "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { "./package.json": "./package.json", ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" + "default": "./dist/index.js" } }, "files": [ "dist/**/*" ], "scripts": { - "build": "tsup", + "build": "vp run compile", "check:typings": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", - "watch": "tsup --watch", + "watch": "vp pack --watch", "clean": "rm -r dist/*", - "test": "vitest run --exclude 'smoke/scenarios/**'", + "test": "vp test run --exclude 'smoke/scenarios/**'", "yalc:publish": "yalc publish" }, "author": "Braintrust Data Inc.", @@ -37,9 +35,9 @@ "@types/node": "^20.10.5", "@types/nunjucks": "^3.2.6", "braintrust": "workspace:*", - "tsup": "^8.5.1", "typescript": "5.5.4", - "vitest": "4.1.11" + "vitest": "catalog:", + "vite-plus": "catalog:" }, "repository": { "type": "git", @@ -51,5 +49,10 @@ "access": "public", "registry": "https://registry.npmjs.org/", "provenance": true - } + }, + "type": "module", + "engines": { + "node": ">=22.18.0" + }, + "sideEffects": false } diff --git a/integrations/templates-nunjucks/smoke/scenarios/jest/Makefile b/integrations/templates-nunjucks/smoke/scenarios/jest/Makefile index 8ee5b0c0c..d9c37e89c 100644 --- a/integrations/templates-nunjucks/smoke/scenarios/jest/Makefile +++ b/integrations/templates-nunjucks/smoke/scenarios/jest/Makefile @@ -9,7 +9,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p js/artifacts && pnpm --filter=braintrust pack --pack-destination js/artifacts; \ + cd ../../../../.. && pnpm --filter=braintrust run build && mkdir -p js/artifacts && pnpm --filter=braintrust pack --pack-destination js/artifacts; \ for f in js/artifacts/braintrust-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-latest.tgz" ] && \ [ "$$(basename $$f)" != "braintrust-otel-latest.tgz" ] && \ @@ -31,7 +31,7 @@ setup: echo "==> Using BRAINTRUST_TEMPLATES_NUNJUCKS_JS_TAR: $(BRAINTRUST_TEMPLATES_NUNJUCKS_JS_TAR)"; \ else \ echo "==> Building @braintrust/templates-nunjucks-js package"; \ - cd ../../../../.. && pnpm exec turbo build --filter=@braintrust/templates-nunjucks-js && cd integrations/templates-nunjucks && pnpm pack --pack-destination ../js/artifacts; \ + cd ../../../../.. && pnpm --filter=@braintrust/templates-nunjucks-js run build && cd integrations/templates-nunjucks && pnpm pack --pack-destination ../../js/artifacts; \ for f in ../../js/artifacts/braintrust-templates-nunjucks-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-templates-nunjucks-js-latest.tgz" ]; then \ cp "$$f" ../../js/artifacts/braintrust-templates-nunjucks-js-latest.tgz; \ @@ -45,4 +45,4 @@ setup: test: setup @echo "==> Running jest tests" @# Run only tests under the local tests/ directory to avoid workspace-wide discovery - pnpm exec jest --passWithNoTests --roots ./tests + NODE_OPTIONS="--experimental-vm-modules" pnpm exec jest --roots ./tests diff --git a/integrations/templates-nunjucks/smoke/scenarios/jest/package.json b/integrations/templates-nunjucks/smoke/scenarios/jest/package.json index 703b21d4d..5936d026f 100644 --- a/integrations/templates-nunjucks/smoke/scenarios/jest/package.json +++ b/integrations/templates-nunjucks/smoke/scenarios/jest/package.json @@ -1,7 +1,7 @@ { "name": "smoke-templates-nunjucks-jest", "private": true, - "type": "commonjs", + "type": "module", "dependencies": { "braintrust": "file:../../../../../js/artifacts/braintrust-latest.tgz", "@braintrust/templates-nunjucks-js": "file:../../../../../js/artifacts/braintrust-templates-nunjucks-js-latest.tgz" @@ -10,7 +10,9 @@ "@types/jest": "^29.5.0", "@types/node": "^20.10.5", "jest": "^29.7.0", - "ts-jest": "^29.1.0", "typescript": "^5.4.4" + }, + "jest": { + "transform": {} } } diff --git a/integrations/templates-nunjucks/smoke/scenarios/jest/tests/jest-basic.test.js b/integrations/templates-nunjucks/smoke/scenarios/jest/tests/jest-basic.test.js index 98aa16d55..57784ebca 100644 --- a/integrations/templates-nunjucks/smoke/scenarios/jest/tests/jest-basic.test.js +++ b/integrations/templates-nunjucks/smoke/scenarios/jest/tests/jest-basic.test.js @@ -1,10 +1,10 @@ -const { +import { runTests, testMustacheTemplate, testNunjucksTemplate, -} = require("../../../../../../js/smoke/shared/dist/index.js"); -const braintrust = require("braintrust"); -const { nunjucksPlugin } = require("@braintrust/templates-nunjucks-js"); +} from "../../../../../../js/smoke/shared/dist/index.js"; +import * as braintrust from "braintrust"; +import { nunjucksPlugin } from "@braintrust/templates-nunjucks-js"; test("Templates-Nunjucks basic behavior", async () => { // Register nunjucks plugin before running tests diff --git a/integrations/templates-nunjucks/smoke/scenarios/nextjs/Makefile b/integrations/templates-nunjucks/smoke/scenarios/nextjs/Makefile index c933a85ef..362577264 100644 --- a/integrations/templates-nunjucks/smoke/scenarios/nextjs/Makefile +++ b/integrations/templates-nunjucks/smoke/scenarios/nextjs/Makefile @@ -9,7 +9,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p js/artifacts && pnpm --filter=braintrust pack --pack-destination js/artifacts; \ + cd ../../../../.. && pnpm --filter=braintrust run build && mkdir -p js/artifacts && pnpm --filter=braintrust pack --pack-destination js/artifacts; \ for f in js/artifacts/braintrust-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-latest.tgz" ] && \ [ "$$(basename $$f)" != "braintrust-otel-latest.tgz" ] && \ @@ -32,7 +32,7 @@ setup: else \ echo "==> Building @braintrust/templates-nunjucks-js package"; \ cd ../../../../.. && \ - pnpm exec turbo build --filter=@braintrust/templates-nunjucks-js && \ + pnpm --filter=@braintrust/templates-nunjucks-js run build && \ cd integrations/templates-nunjucks && \ pnpm pack --pack-destination ../../js/artifacts && \ cd ../../js/artifacts && \ diff --git a/integrations/templates-nunjucks/smoke/scenarios/node-esm/Makefile b/integrations/templates-nunjucks/smoke/scenarios/node-esm/Makefile index f3416ca1d..1d2234310 100644 --- a/integrations/templates-nunjucks/smoke/scenarios/node-esm/Makefile +++ b/integrations/templates-nunjucks/smoke/scenarios/node-esm/Makefile @@ -13,7 +13,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p js/artifacts && pnpm --filter=braintrust pack --pack-destination js/artifacts; \ + cd ../../../../.. && pnpm --filter=braintrust run build && mkdir -p js/artifacts && pnpm --filter=braintrust pack --pack-destination js/artifacts; \ for f in js/artifacts/braintrust-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-latest.tgz" ] && \ [ "$$(basename $$f)" != "braintrust-otel-latest.tgz" ] && \ @@ -35,7 +35,7 @@ setup: echo "==> Using BRAINTRUST_TEMPLATES_NUNJUCKS_JS_TAR: $(BRAINTRUST_TEMPLATES_NUNJUCKS_JS_TAR)"; \ else \ echo "==> Building @braintrust/templates-nunjucks-js package"; \ - cd ../../../../.. && pnpm exec turbo build --filter=@braintrust/templates-nunjucks-js && cd integrations/templates-nunjucks && pnpm pack --pack-destination ../js/artifacts; \ + cd ../../../../.. && pnpm --filter=@braintrust/templates-nunjucks-js run build && cd integrations/templates-nunjucks && pnpm pack --pack-destination ../../js/artifacts; \ for f in ../../js/artifacts/braintrust-templates-nunjucks-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-templates-nunjucks-js-latest.tgz" ]; then \ cp "$$f" ../../js/artifacts/braintrust-templates-nunjucks-js-latest.tgz; \ diff --git a/integrations/templates-nunjucks/smoke/scenarios/node-esm/tests/basic.test.ts b/integrations/templates-nunjucks/smoke/scenarios/node-esm/tests/basic.test.ts index 0e53dce22..1580fd347 100644 --- a/integrations/templates-nunjucks/smoke/scenarios/node-esm/tests/basic.test.ts +++ b/integrations/templates-nunjucks/smoke/scenarios/node-esm/tests/basic.test.ts @@ -2,7 +2,7 @@ import { runTests, testMustacheTemplate, testNunjucksTemplate, -} from "../../../../../../js/smoke/shared/dist/index.mjs"; +} from "../../../../../../js/smoke/shared/dist/index.js"; import * as braintrust from "braintrust"; import { nunjucksPlugin } from "@braintrust/templates-nunjucks-js"; diff --git a/integrations/templates-nunjucks/src/index.test.ts b/integrations/templates-nunjucks/src/index.test.ts index 7323d1224..06bda0c4c 100644 --- a/integrations/templates-nunjucks/src/index.test.ts +++ b/integrations/templates-nunjucks/src/index.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, beforeAll } from "vitest"; +import { describe, test, expect, beforeAll } from "vite-plus/test"; import { registerTemplatePlugin, Prompt } from "braintrust"; import { nunjucksPlugin } from "./index"; diff --git a/integrations/templates-nunjucks/tsconfig.json b/integrations/templates-nunjucks/tsconfig.json index e0b228935..36f6bb648 100644 --- a/integrations/templates-nunjucks/tsconfig.json +++ b/integrations/templates-nunjucks/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["es2022"], "module": "esnext", "target": "es2022", - "moduleResolution": "node", + "moduleResolution": "bundler", "strict": true, "noUnusedLocals": true, "esModuleInterop": true, diff --git a/integrations/templates-nunjucks/tsup.config.ts b/integrations/templates-nunjucks/tsup.config.ts deleted file mode 100644 index 41099bc7a..000000000 --- a/integrations/templates-nunjucks/tsup.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - format: ["cjs", "esm"], - outDir: "dist", - dts: true, - splitting: true, - clean: true, -}); diff --git a/integrations/templates-nunjucks/vite.config.ts b/integrations/templates-nunjucks/vite.config.ts new file mode 100644 index 000000000..0740d0c48 --- /dev/null +++ b/integrations/templates-nunjucks/vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + test: { + reporters: ["default"], + include: ["tests/**/*.test.ts", "src/**/*.test.ts"], + }, + run: { + tasks: { + compile: { + command: "vp pack", + dependsOn: [ + { task: "compile", from: ["dependencies", "devDependencies"] }, + ], + cache: { + input: [{ auto: true }, "!dist/**"], + untrackedEnv: ["SHELL", "BASH_ENV"], + output: ["dist/**"], + }, + }, + }, + }, + pack: { + dts: { generator: "oxc" }, + sourcemap: true, + fixedExtension: false, + }, +}); diff --git a/integrations/templates-nunjucks/vitest.config.ts b/integrations/templates-nunjucks/vitest.config.ts deleted file mode 100644 index 744610c82..000000000 --- a/integrations/templates-nunjucks/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - reporters: ["default"], - include: ["tests/**/*.test.ts", "src/**/*.test.ts"], - }, -}); diff --git a/integrations/temporal-js/.gitignore b/integrations/temporal-js/.gitignore new file mode 100644 index 000000000..7a6c09645 --- /dev/null +++ b/integrations/temporal-js/.gitignore @@ -0,0 +1 @@ +.vitest/ diff --git a/integrations/temporal-js/examples/temporal-ai-sdk/mise.toml b/integrations/temporal-js/examples/temporal-ai-sdk/mise.toml index 779401728..f905457b0 100644 --- a/integrations/temporal-js/examples/temporal-ai-sdk/mise.toml +++ b/integrations/temporal-js/examples/temporal-ai-sdk/mise.toml @@ -1,5 +1,5 @@ [settings] -experimental=true +experimental = true [env] _.file = ".env" diff --git a/integrations/temporal-js/examples/temporal-cjs/mise.toml b/integrations/temporal-js/examples/temporal-cjs/mise.toml index f7cfa1570..14fed2359 100644 --- a/integrations/temporal-js/examples/temporal-cjs/mise.toml +++ b/integrations/temporal-js/examples/temporal-cjs/mise.toml @@ -1,6 +1,6 @@ # Mise will automatically read and use .tool-versions files as well as this file. [settings] -experimental=true +experimental = true [env] # See env.example to configure API keys. diff --git a/integrations/temporal-js/examples/temporal-esm/mise.toml b/integrations/temporal-js/examples/temporal-esm/mise.toml index 53cd879dc..7f6bf10b8 100644 --- a/integrations/temporal-js/examples/temporal-esm/mise.toml +++ b/integrations/temporal-js/examples/temporal-esm/mise.toml @@ -1,5 +1,5 @@ [settings] -experimental=true +experimental = true [env] _.file = ".env" diff --git a/integrations/temporal-js/package.json b/integrations/temporal-js/package.json index 569528886..d5926dadb 100644 --- a/integrations/temporal-js/package.json +++ b/integrations/temporal-js/package.json @@ -3,29 +3,25 @@ "version": "1.0.1", "description": "SDK for integrating Braintrust with Temporal", "main": "./dist/index.js", - "module": "./dist/index.mjs", + "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { "./package.json": "./package.json", ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" + "default": "./dist/index.js" }, "./workflow-interceptors": { "types": "./dist/workflow-interceptors.d.ts", - "import": "./dist/workflow-interceptors.mjs", - "module": "./dist/workflow-interceptors.mjs", - "require": "./dist/workflow-interceptors.js" + "default": "./dist/workflow-interceptors.js" } }, "scripts": { - "build": "tsup", + "build": "vp run compile", "check:typings": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", - "watch": "tsup --watch", + "watch": "vp pack --watch", "clean": "rm -r dist/*", - "test": "vitest run", + "test": "vp test run", "yalc:publish": "yalc publish" }, "author": "Braintrust Data Inc.", @@ -38,9 +34,9 @@ "@temporalio/workflow": "^1.14.1", "@types/node": "^22.15.21", "braintrust": "workspace:*", - "tsup": "^8.5.0", "typescript": "5.5.4", - "vitest": "4.1.11" + "vitest": "catalog:", + "vite-plus": "catalog:" }, "peerDependencies": { "@temporalio/activity": ">=1.11.0", @@ -80,5 +76,10 @@ "access": "public", "registry": "https://registry.npmjs.org/", "provenance": true - } + }, + "type": "module", + "engines": { + "node": ">=22.18.0" + }, + "sideEffects": false } diff --git a/integrations/temporal-js/src/temporal.test.ts b/integrations/temporal-js/src/temporal.test.ts index f8a8732f4..28fc9cd6e 100644 --- a/integrations/temporal-js/src/temporal.test.ts +++ b/integrations/temporal-js/src/temporal.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe } from "vitest"; +import { expect, test, describe } from "vite-plus/test"; import { serializeHeaderValue, deserializeHeaderValue, diff --git a/integrations/temporal-js/tsup.config.ts b/integrations/temporal-js/tsup.config.ts deleted file mode 100644 index 6563ce288..000000000 --- a/integrations/temporal-js/tsup.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig([ - { - entry: ["src/index.ts", "src/workflow-interceptors.ts"], - format: ["cjs", "esm"], - outDir: "dist", - external: [ - "braintrust", - "@braintrust/temporal/workflow-interceptors", - "@temporalio/activity", - "@temporalio/client", - "@temporalio/common", - "@temporalio/worker", - "@temporalio/workflow", - ], - dts: true, - }, -]); diff --git a/integrations/temporal-js/vite.config.ts b/integrations/temporal-js/vite.config.ts new file mode 100644 index 000000000..c93c1918c --- /dev/null +++ b/integrations/temporal-js/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + test: { + reporters: ["default"], + include: ["tests/**/*.test.ts", "src/**/*.test.ts"], + }, + run: { + tasks: { + compile: { + command: "vp pack", + dependsOn: [ + { task: "compile", from: ["dependencies", "devDependencies"] }, + ], + cache: { + input: [{ auto: true }, "!dist/**"], + untrackedEnv: ["SHELL", "BASH_ENV"], + output: ["dist/**"], + }, + }, + }, + }, + pack: { + entry: ["src/index.ts", "src/workflow-interceptors.ts"], + deps: { neverBundle: ["@braintrust/temporal/workflow-interceptors"] }, + dts: { generator: "oxc" }, + sourcemap: true, + fixedExtension: false, + }, +}); diff --git a/integrations/temporal-js/vitest.config.ts b/integrations/temporal-js/vitest.config.ts deleted file mode 100644 index 744610c82..000000000 --- a/integrations/temporal-js/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - reporters: ["default"], - include: ["tests/**/*.test.ts", "src/**/*.test.ts"], - }, -}); diff --git a/js/.gitignore b/js/.gitignore index ccf7d9ef2..a2982dec2 100644 --- a/js/.gitignore +++ b/js/.gitignore @@ -13,3 +13,4 @@ braintrust-*.tgz vendor/ playground.ts +.vitest/ diff --git a/js/NOTICE b/js/NOTICE index 71d1b1af2..5c78d8304 100644 --- a/js/NOTICE +++ b/js/NOTICE @@ -24,3 +24,8 @@ diagnostics_channel TracingChannel implementation. Copyright Node.js contributors. All rights reserved. It is licensed under the MIT License. See licenses/node-diagnostics-channel/LICENSE. + +This package also bundles async 3.2.5. +Copyright (c) 2010-2018 Caolan McMahon. +async is licensed under the MIT License. +See licenses/async/LICENSE. diff --git a/js/eslint.config.ts b/js/eslint.config.ts deleted file mode 100644 index 00dad5c0a..000000000 --- a/js/eslint.config.ts +++ /dev/null @@ -1,230 +0,0 @@ -import tseslint from "@typescript-eslint/eslint-plugin"; -import tsparser from "@typescript-eslint/parser"; -import esX from "eslint-plugin-es-x"; -import n from "eslint-plugin-n"; -import nodeImport from "eslint-plugin-node-import"; -import tsupConfigImport from "./tsup.config"; - -const supportedNodeVersionRange = "^20.12.0 || ^21.7.0 || ^22.13.0 || >=23.5.0"; - -// Handle both ESM and CJS module formats -const tsupConfig = Array.isArray(tsupConfigImport) - ? tsupConfigImport - : tsupConfigImport.default || []; - -// Extract entry points from tsup config -const entryFiles = tsupConfig.flatMap((config) => { - const entries = Array.isArray(config.entry) - ? config.entry - : Object.values(config.entry); - return entries; -}); - -const generatedTypeImports = { - group: [ - "**/generated_types", - "**/generated_plain_types", - "**/generated_types.ts", - "**/generated_plain_types.ts", - ], - message: - "Generated backend definitions are compatibility-test fixtures. Use SDK-owned types and validators instead.", -}; - -export default [ - { - ignores: [ - "dist/**", - "util/dist/**", - "node_modules/**", - "vendor/**", - "examples/**", - "**/generated*_types.ts", - "scripts/**", - ".turbo/**", - "docs/**", - "test-ai-sdk-wrapper/**", - "vercel/**", - // TODO: Add these back once tsconfig.json includes them, so that - // typed linting (and all other config blocks) can run on them too. - "**/*.test.ts", - "**/*.test.tsx", - "src/auto-instrumentations/**", - ], - }, - { - files: ["src/**/*.ts", "src/**/*.tsx"], - languageOptions: { - parser: tsparser, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: "./tsconfig.json", - }, - }, - plugins: { - "@typescript-eslint": tseslint, - "es-x": esX, - n, - "node-import": nodeImport, - }, - rules: { - // Base TypeScript rules - ...tseslint.configs.recommended.rules, - ...esX.configs["flat/restrict-to-es2022"].rules, - // TODO: Fix violations and re-enable as "error" - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-unused-vars": [ - "error", - { - vars: "all", - args: "none", - ignoreRestSiblings: false, - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - }, - ], - "prefer-const": "error", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-require-imports": "off", - // TODO: Fix violations and re-enable as "error" - "@typescript-eslint/consistent-type-assertions": [ - "warn", - { assertionStyle: "never" }, - ], - "no-unused-expressions": ["error", { allowShortCircuit: true }], - "@typescript-eslint/no-unused-expressions": "off", - "@typescript-eslint/no-empty-object-type": "error", - "@typescript-eslint/no-unsafe-function-type": "error", - "@typescript-eslint/prefer-as-const": "error", - "no-restricted-properties": [ - "error", - { - object: "console", - property: "log", - message: "Use debugLogger instead of console for SDK logging.", - }, - { - object: "console", - property: "warn", - message: "Use debugLogger instead of console for SDK logging.", - }, - { - object: "console", - property: "error", - message: "Use debugLogger instead of console for SDK logging.", - }, - { - object: "console", - property: "debug", - message: "Use debugLogger instead of console for SDK logging.", - }, - { - object: "console", - property: "info", - message: "Use debugLogger instead of console for SDK logging.", - }, - { - object: "console", - property: "trace", - message: "Use debugLogger instead of console for SDK logging.", - }, - ], - "no-restricted-syntax": [ - "error", - { - selector: - "CallExpression[callee.type='MemberExpression'][callee.property.type='Identifier'][callee.property.name='enterWith']", - message: - "AsyncLocalStorage.enterWith() is not supported in Cloudflare Workers. Use AsyncLocalStorage.run() to scope context instead.", - }, - { - selector: - "CallExpression[callee.type='MemberExpression'][callee.property.type='Literal'][callee.property.value='enterWith']", - message: - "AsyncLocalStorage.enterWith() is not supported in Cloudflare Workers. Use AsyncLocalStorage.run() to scope context instead.", - }, - ], - "n/no-unsupported-features/node-builtins": [ - "error", - { - version: supportedNodeVersionRange, - allowExperimental: true, - }, - ], - "n/no-unsupported-features/es-builtins": [ - "error", - { version: supportedNodeVersionRange }, - ], - "n/no-unsupported-features/es-syntax": [ - "error", - { version: supportedNodeVersionRange }, - ], - // Require node: protocol for Node.js built-in imports (for Deno compatibility) - // This plugin automatically detects ALL Node.js built-ins - no manual list needed! - "node-import/prefer-node-protocol": "error", - }, - }, - { - files: ["src/queue.bench.ts"], - rules: { - "no-restricted-properties": "off", - }, - }, - { - files: ["src/**/*.ts", "src/**/*.tsx", "util/**/*.ts"], - languageOptions: { - parser: tsparser, - }, - plugins: { - "@typescript-eslint": tseslint, - }, - rules: { - "no-restricted-imports": ["error", { patterns: [generatedTypeImports] }], - }, - }, - { - files: ["src/**/*.ts", "src/**/*.tsx"], - ignores: [...entryFiles, "**/*.test.ts", "**/*.test.tsx"], - rules: { - "no-restricted-imports": [ - "error", - { - patterns: [ - generatedTypeImports, - { - group: [ - "./exports", - "./exports.ts", - "../exports", - "../exports.ts", - ], - message: - "Direct imports from 'exports.ts' are not allowed. Import from the specific module instead. Only entry points (index.ts, browser.ts) should import from exports.ts.", - }, - ], - }, - ], - }, - }, - { - files: [ - ...entryFiles, - // Also include exports files which are imported by entry points - "src/exports.ts", - ], - ignores: ["**/*.test.ts", "**/*.test.tsx"], - languageOptions: { - parser: tsparser, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: "./tsconfig.json", - }, - }, - plugins: { - "@typescript-eslint": tseslint, - }, - }, -]; diff --git a/js/licenses/async/LICENSE b/js/licenses/async/LICENSE new file mode 100644 index 000000000..b18aed692 --- /dev/null +++ b/js/licenses/async/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2018 Caolan McMahon + +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/js/package.json b/js/package.json index 3be3f3e3b..566521f4d 100644 --- a/js/package.json +++ b/js/package.json @@ -8,116 +8,100 @@ "directory": "js" }, "homepage": "https://www.braintrust.dev/docs", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "sideEffects": true, - "browser": { - "./dist/index.js": "./dist/browser.js", - "./dist/index.d.ts": "./dist/browser.d.ts", - "./dist/index.mjs": "./dist/browser.mjs", - "./dist/index.d.mts": "./dist/browser.d.mts" - }, + "main": "./dist/node/index.js", + "module": "./dist/node/index.js", + "types": "./dist/node/index.d.ts", + "sideEffects": [ + "./dist/*/auto.js", + "./dist/node/apply-auto-instrumentation.js", + "./dist/hooks/hook.mjs" + ], "exports": { - "./package.json": "./package.json", ".": { - "types": "./dist/index.d.ts", - "edge-light": "./dist/edge-light.mjs", - "workerd": "./dist/workerd.mjs", - "node": { - "import": "./dist/index.mjs", - "require": "./dist/index.js" - }, - "browser": "./dist/browser.mjs", - "import": "./dist/index.mjs", - "require": "./dist/index.js", - "default": "./dist/index.mjs" + "types": "./dist/node/index.d.ts", + "workerd": "./dist/workerd/index.js", + "edge-light": "./dist/edge-light/index.js", + "browser": "./dist/browser/index.js", + "node": "./dist/node/index.js", + "default": "./dist/browser/index.js" + }, + "./package.json": "./package.json", + "./auto": { + "types": "./dist/node/auto.d.ts", + "workerd": "./dist/workerd/auto.js", + "edge-light": "./dist/edge-light/auto.js", + "browser": "./dist/browser/auto.js", + "node": "./dist/node/auto.js", + "default": "./dist/browser/auto.js" }, "./custom-views": { - "types": "./dist/custom-views.d.ts", - "import": "./dist/custom-views.mjs", - "module": "./dist/custom-views.mjs", - "require": "./dist/custom-views.js", - "default": "./dist/custom-views.mjs" + "types": "./dist/node/custom-views.d.ts", + "workerd": "./dist/workerd/custom-views.js", + "edge-light": "./dist/edge-light/custom-views.js", + "browser": "./dist/browser/custom-views.js", + "node": "./dist/node/custom-views.js", + "default": "./dist/browser/custom-views.js" }, "./preprocessors": { - "types": "./dist/preprocessors.d.ts", - "import": "./dist/preprocessors.mjs", - "module": "./dist/preprocessors.mjs", - "require": "./dist/preprocessors.js", - "default": "./dist/preprocessors.mjs" + "types": "./dist/node/preprocessors.d.ts", + "workerd": "./dist/workerd/preprocessors.js", + "edge-light": "./dist/edge-light/preprocessors.js", + "browser": "./dist/browser/preprocessors.js", + "node": "./dist/node/preprocessors.js", + "default": "./dist/browser/preprocessors.js" + }, + "./instrumentation": { + "types": "./dist/node/instrumentation.d.ts", + "workerd": "./dist/workerd/instrumentation.js", + "edge-light": "./dist/edge-light/instrumentation.js", + "browser": "./dist/browser/instrumentation.js", + "node": "./dist/node/instrumentation.js", + "default": "./dist/browser/instrumentation.js" + }, + "./node": { + "types": "./dist/node/index.d.ts", + "default": "./dist/node/index.js" }, "./workerd": { - "import": "./dist/workerd.mjs", - "require": "./dist/workerd.js", - "default": "./dist/workerd.mjs" + "types": "./dist/workerd/index.d.ts", + "default": "./dist/workerd/index.js" }, "./edge-light": { - "import": "./dist/edge-light.mjs", - "require": "./dist/edge-light.js", - "default": "./dist/edge-light.mjs" + "types": "./dist/edge-light/index.d.ts", + "default": "./dist/edge-light/index.js" }, "./apply-auto-instrumentation": { - "types": "./dist/apply-auto-instrumentation.d.ts", - "edge-light": "./dist/apply-auto-instrumentation.browser.mjs", - "workerd": "./dist/apply-auto-instrumentation.browser.mjs", - "node": { - "import": "./dist/apply-auto-instrumentation.mjs", - "require": "./dist/apply-auto-instrumentation.js" - }, - "browser": "./dist/apply-auto-instrumentation.browser.mjs", - "import": "./dist/apply-auto-instrumentation.mjs", - "require": "./dist/apply-auto-instrumentation.js", - "default": "./dist/apply-auto-instrumentation.mjs" + "types": "./dist/node/apply-auto-instrumentation.d.ts", + "workerd": "./dist/workerd/apply-auto-instrumentation.js", + "edge-light": "./dist/edge-light/apply-auto-instrumentation.js", + "browser": "./dist/browser/apply-auto-instrumentation.js", + "node": "./dist/node/apply-auto-instrumentation.js", + "default": "./dist/browser/apply-auto-instrumentation.js" }, "./vitest-evals-reporter": { - "types": "./dist/vitest-evals-reporter.d.ts", - "import": "./dist/vitest-evals-reporter.mjs", - "module": "./dist/vitest-evals-reporter.mjs", - "require": "./dist/vitest-evals-reporter.js" - }, - "./node": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" - }, - "./instrumentation": { - "types": "./dist/instrumentation/index.d.ts", - "import": "./dist/instrumentation/index.mjs", - "module": "./dist/instrumentation/index.mjs", - "require": "./dist/instrumentation/index.js" + "types": "./dist/node/vitest-evals-reporter.d.ts", + "default": "./dist/node/vitest-evals-reporter.js" }, - "./hook.mjs": "./dist/auto-instrumentations/hook.mjs", + "./hook.mjs": "./dist/hooks/hook.mjs", "./vite": { - "types": "./dist/auto-instrumentations/bundler/vite.d.ts", - "import": "./dist/auto-instrumentations/bundler/vite.mjs", - "module": "./dist/auto-instrumentations/bundler/vite.mjs", - "require": "./dist/auto-instrumentations/bundler/vite.cjs" + "types": "./dist/tooling/bundler/vite.d.ts", + "default": "./dist/tooling/bundler/vite.js" }, "./webpack": { - "types": "./dist/auto-instrumentations/bundler/webpack.d.ts", - "import": "./dist/auto-instrumentations/bundler/webpack.mjs", - "module": "./dist/auto-instrumentations/bundler/webpack.mjs", - "require": "./dist/auto-instrumentations/bundler/webpack.cjs" + "types": "./dist/tooling/bundler/webpack.d.ts", + "default": "./dist/tooling/bundler/webpack.js" }, "./next": { - "types": "./dist/auto-instrumentations/bundler/next.d.ts", - "import": "./dist/auto-instrumentations/bundler/next.mjs", - "module": "./dist/auto-instrumentations/bundler/next.mjs", - "require": "./dist/auto-instrumentations/bundler/next.cjs" + "types": "./dist/tooling/bundler/next.d.ts", + "default": "./dist/tooling/bundler/next.js" }, "./esbuild": { - "types": "./dist/auto-instrumentations/bundler/esbuild.d.ts", - "import": "./dist/auto-instrumentations/bundler/esbuild.mjs", - "module": "./dist/auto-instrumentations/bundler/esbuild.mjs", - "require": "./dist/auto-instrumentations/bundler/esbuild.cjs" + "types": "./dist/tooling/bundler/esbuild.d.ts", + "default": "./dist/tooling/bundler/esbuild.js" }, "./rollup": { - "types": "./dist/auto-instrumentations/bundler/rollup.d.ts", - "import": "./dist/auto-instrumentations/bundler/rollup.mjs", - "module": "./dist/auto-instrumentations/bundler/rollup.mjs", - "require": "./dist/auto-instrumentations/bundler/rollup.cjs" + "types": "./dist/tooling/bundler/rollup.d.ts", + "default": "./dist/tooling/bundler/rollup.js" } }, "files": [ @@ -126,33 +110,33 @@ "licenses/**/*" ], "scripts": { - "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsup", + "build": "vp run compile", "check:typings": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", - "watch": "tsup --watch", + "watch": "vp pack --watch", "clean": "rm -r dist/*", "docs": "typedoc --options typedoc.json src/node/index.ts", - "test": "vitest run --exclude \"src/wrappers/**/*.test.ts\" --exclude \"src/otel/**/*.test.ts\" --exclude \"smoke/**/*.test.ts\" --exclude \"tests/api-compatibility/**\"", + "test": "vp test run --exclude \"src/wrappers/**/*.test.ts\" --exclude \"src/otel/**/*.test.ts\" --exclude \"smoke/**/*.test.ts\" --exclude \"tests/api-compatibility/**\"", "test:core": "pnpm prune && pnpm test", "test:checks": "pnpm run test:core && pnpm run test:vitest", "test:all": "pnpm run test:checks", - "test:api-compat": "vitest run tests/api-compatibility/api-compatibility.test.ts", - "test:otel": "vitest run --dir src/otel", - "test:otel-no-deps": "vitest run src/otel/otel-no-deps.test.ts --reporter=verbose", - "test:ai-sdk-v1": "vitest run src/wrappers/ai-sdk-v1.test.ts", - "test:ai-sdk-v2": "vitest run src/wrappers/ai-sdk-v2.test.ts src/wrappers/ai-sdk-v1.test.ts", - "test:mastra": "vitest run src/wrappers/mastra.test.ts", + "test:api-compat": "vp test run tests/api-compatibility/api-compatibility.test.ts", + "test:otel": "vp test run --dir src/otel", + "test:otel-no-deps": "vp test run src/otel/otel-no-deps.test.ts --reporter=verbose", + "test:ai-sdk-v1": "vp test run src/wrappers/ai-sdk-v1.test.ts", + "test:ai-sdk-v2": "vp test run src/wrappers/ai-sdk-v2.test.ts src/wrappers/ai-sdk-v1.test.ts", + "test:mastra": "vp test run src/wrappers/mastra.test.ts", "test:vitest": "pnpm --filter @braintrust/vitest-wrapper-tests test", "bench": "tsx src/queue.bench.ts", - "lint": "eslint .", - "fix:lint": "eslint --fix .", + "lint": "vp lint .", + "fix:lint": "vp lint --fix .", "playground": "tsx playground.ts", - "playground:auto": "mkdir -p .context && pnpm exec esbuild playground.ts --platform=node --format=esm --outfile=.context/playground.auto.mjs && node --import ./dist/auto-instrumentations/hook.mjs ./.context/playground.auto.mjs", + "playground:auto": "mkdir -p .context && pnpm exec esbuild playground.ts --platform=node --format=esm --outfile=.context/playground.auto.mjs && node --import ./dist/hooks/hook.mjs ./.context/playground.auto.mjs", "yalc:publish": "yalc publish" }, "author": "", "license": "MIT", "engines": { - "node": "^20.12.0 || >=22.13.0" + "node": ">=22.18.0" }, "devDependencies": { "@anthropic-ai/sdk": "^0.60.0", @@ -161,30 +145,25 @@ "@types/mustache": "^4.2.5", "@types/node": "^20.10.5", "@types/react": "^18.3.1", - "@typescript-eslint/eslint-plugin": "^8.49.0", - "@typescript-eslint/parser": "^8.49.0", "ai": "^6.0.0", "async": "^3.2.5", "cross-env": "^7.0.3", - "eslint": "^9.39.2", - "eslint-plugin-es-x": "9.7.0", + "esbuild": "0.28.1", "eslint-plugin-n": "^18.2.1", - "eslint-plugin-node-import": "^1.0.5", "openai": "6.25.0", "rollup": "^4.60.3", "tar": "^7.5.22", "tinybench": "^4.0.1", "ts-declaration-location": "^1.0.7", - "tsup": "^8.5.1", "tsx": "^4.21.0", "typedoc": "^0.25.13", "typedoc-plugin-markdown": "^3.17.1", "typescript": "5.4.4", - "vite": "^8.1.5", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "4.1.11", + "vite": "catalog:", + "vite-plus": "catalog:", "webpack": "^5.106.2", - "zod": "^3.25.34" + "zod": "^3.25.34", + "vitest": "catalog:" }, "dependencies": { "@vercel/functions": "^1.0.2", @@ -194,7 +173,6 @@ "astring": "^1.9.0", "cjs-module-lexer": "^2.2.0", "dotenv": "^16.4.5", - "esbuild": "0.28.1", "esquery": "^1.7.0", "eventsource-parser": "^1.1.2", "meriyah": "^6.1.4", @@ -219,5 +197,6 @@ "access": "public", "registry": "https://registry.npmjs.org/", "provenance": true - } + }, + "type": "module" } diff --git a/js/scripts/benchmark-imports.mjs b/js/scripts/benchmark-imports.mjs new file mode 100644 index 000000000..e4870e49a --- /dev/null +++ b/js/scripts/benchmark-imports.mjs @@ -0,0 +1,85 @@ +import { execFileSync } from "node:child_process"; +import { mkdir, writeFile, symlink } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { build } from "esbuild"; +import { gzipSync, brotliCompressSync } from "node:zlib"; + +const root = fileURLToPath(new URL("../../", import.meta.url)); +const outputDir = path.join(root, ".context/bundling"); +await mkdir(outputDir, { recursive: true }); +const baseline = process.argv[2]; +const entries = { + root: path.join(root, "js/dist/node/index.js"), + ...(baseline ? { baseline: path.resolve(baseline) } : {}), +}; +// An optional baseline build uses the same installed runtime dependency versions. +if (baseline) { + try { + await symlink( + path.join(root, "js/node_modules"), + path.join(path.dirname(baseline), "node_modules"), + "dir", + ); + } catch (error) { + if (error.code !== "EEXIST") throw error; + } +} +const report = {}; +for (const [name, entry] of Object.entries(entries)) { + const samples = []; + for (let index = 0; index < 15; index++) { + const sample = execFileSync( + process.execPath, + [ + "--input-type=module", + "-e", + ` + const start = performance.now(); + const sdk = await import(${JSON.stringify(entry)}); + const imported = performance.now(); + sdk.currentSpan(); + const used = performance.now(); + console.log(JSON.stringify({ importMs: imported - start, firstUseMs: used - imported, rss: process.memoryUsage().rss })); + process.exit(0); + `, + ], + { encoding: "utf8", env: { ...process.env, BRAINTRUST_DEBUG: "false" } }, + ); + samples.push(JSON.parse(sample)); + } + const sorted = (key) => + samples.map((sample) => sample[key]).sort((a, b) => a - b); + report[name] = { + importMedianMs: sorted("importMs")[7], + importP95Ms: sorted("importMs")[14], + firstUseMedianMs: sorted("firstUseMs")[7], + rssMedianBytes: sorted("rss")[7], + samples, + }; + const bundle = await build({ + stdin: { + contents: `import { traced } from ${JSON.stringify(entry)}; globalThis.run = traced;`, + resolveDir: root, + }, + nodePaths: [path.join(root, "js/node_modules")], + bundle: true, + minify: true, + platform: "node", + format: "esm", + write: false, + logLevel: "silent", + }); + const bytes = bundle.outputFiles[0].contents; + report[name].bundle = { + bytes: bytes.length, + gzip: gzipSync(bytes).length, + brotli: brotliCompressSync(bytes).length, + }; +} +await writeFile( + path.join(outputDir, "import-benchmark.json"), + JSON.stringify(report, null, 2) + "\n", +); +for (const [name, { samples, ...summary }] of Object.entries(report)) + console.log(name, summary); diff --git a/js/smoke/Makefile b/js/smoke/Makefile index c610ebee5..4a8e9fe5c 100644 --- a/js/smoke/Makefile +++ b/js/smoke/Makefile @@ -57,10 +57,10 @@ setup: echo "==> Using provided BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK and creating tarball"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && pnpm --filter=braintrust pack --pack-destination sdk/js/artifacts; \ - for f in sdk/js/artifacts/braintrust-*.tgz; do \ + cd ../.. && pnpm --filter=braintrust run build && pnpm --filter=braintrust pack --pack-destination js/artifacts; \ + for f in js/artifacts/braintrust-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-latest.tgz" ] && [ "$$(basename $$f)" != "braintrust-otel-latest.tgz" ] && [ "$$(basename $$f)" != "braintrust-browser-latest.tgz" ]; then \ - cp "$$f" sdk/js/artifacts/braintrust-latest.tgz; \ + cp "$$f" js/artifacts/braintrust-latest.tgz; \ break; \ fi; \ done; \ @@ -71,10 +71,10 @@ setup: echo "==> Using provided BRAINTRUST_BROWSER_TAR: $(BRAINTRUST_BROWSER_TAR)"; \ else \ echo "==> Building browser package and creating tarball"; \ - cd ../../.. && pnpm exec turbo build --filter=@braintrust/browser && pnpm --filter=@braintrust/browser pack --pack-destination sdk/js/artifacts; \ - for f in sdk/js/artifacts/braintrust-browser-*.tgz; do \ + cd ../.. && pnpm --filter=@braintrust/browser run build && pnpm --filter=@braintrust/browser pack --pack-destination js/artifacts; \ + for f in js/artifacts/braintrust-browser-*.tgz; do \ if [ "$$(basename $$f)" != "braintrust-browser-latest.tgz" ]; then \ - cp "$$f" sdk/js/artifacts/braintrust-browser-latest.tgz; \ + cp "$$f" js/artifacts/braintrust-browser-latest.tgz; \ break; \ fi; \ done; \ diff --git a/js/smoke/README.md b/js/smoke/README.md index 72a0fd530..2b47615e8 100644 --- a/js/smoke/README.md +++ b/js/smoke/README.md @@ -29,7 +29,7 @@ import { testBasicSpanLogging, testMultipleSpans, // ... other test functions -} from "../../shared/dist/index.mjs"; +} from "../../shared/dist/index.js"; import * as braintrust from "braintrust"; diff --git a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/Makefile b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/Makefile index cdc0573f2..1f9f42d3a 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/Makefile +++ b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/Makefile @@ -8,7 +8,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ [ "$$(basename $$f)" = "braintrust-latest.tgz" ] && continue; \ diff --git a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs index a5af1c383..9314d2ac1 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs +++ b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev-node-esm/tests/node-esm-import.test.mjs @@ -17,10 +17,7 @@ import { spawn } from "node:child_process"; import { rmSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import { - displayTestResults, - hasFailures, -} from "../../../shared/dist/index.mjs"; +import { displayTestResults, hasFailures } from "../../../shared/dist/index.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/Makefile b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/Makefile index d947f67c2..666033c40 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/Makefile +++ b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/Makefile @@ -8,7 +8,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ [ "$$(basename $$f)" = "braintrust-latest.tgz" ] && continue; \ diff --git a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/tests/vite-dev.test.mjs b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/tests/vite-dev.test.mjs index e9c1a0dc8..7cce14856 100755 --- a/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/tests/vite-dev.test.mjs +++ b/js/smoke/scenarios/cloudflare-vite-hono-vite-dev/tests/vite-dev.test.mjs @@ -9,7 +9,7 @@ import { spawn, execSync } from "node:child_process"; import { rmSync } from "node:fs"; -import { displayTestResults } from "../../../shared/dist/index.mjs"; +import { displayTestResults } from "../../../shared/dist/index.js"; const PORT = 5173; const MAX_RETRIES = 40; diff --git a/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/Makefile b/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/Makefile index e27f0b111..f698ea410 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/Makefile +++ b/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/Makefile @@ -8,7 +8,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ [ "$$(basename $$f)" = "braintrust-latest.tgz" ] && continue; \ diff --git a/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/tests/worker.test.mjs b/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/tests/worker.test.mjs index 953386606..dbf0d8c9a 100644 --- a/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/tests/worker.test.mjs +++ b/js/smoke/scenarios/cloudflare-vite-hono-wrangler-dev/tests/worker.test.mjs @@ -1,8 +1,5 @@ import { spawn, execSync } from "node:child_process"; -import { - displayTestResults, - hasFailures, -} from "../../../shared/dist/index.mjs"; +import { displayTestResults, hasFailures } from "../../../shared/dist/index.js"; const PORT = 8800; const MAX_RETRIES = 20; diff --git a/js/smoke/scenarios/cloudflare-worker-browser-compat/Makefile b/js/smoke/scenarios/cloudflare-worker-browser-compat/Makefile index 4d52e473f..57dd745e8 100644 --- a/js/smoke/scenarios/cloudflare-worker-browser-compat/Makefile +++ b/js/smoke/scenarios/cloudflare-worker-browser-compat/Makefile @@ -8,7 +8,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ [ "$$(basename $$f)" = "braintrust-latest.tgz" ] && continue; \ diff --git a/js/smoke/scenarios/cloudflare-worker-browser-compat/tests/worker.test.mjs b/js/smoke/scenarios/cloudflare-worker-browser-compat/tests/worker.test.mjs index bd006d9ee..ca9177e89 100644 --- a/js/smoke/scenarios/cloudflare-worker-browser-compat/tests/worker.test.mjs +++ b/js/smoke/scenarios/cloudflare-worker-browser-compat/tests/worker.test.mjs @@ -1,8 +1,5 @@ import { spawn, execSync } from "node:child_process"; -import { - displayTestResults, - hasFailures, -} from "../../../shared/dist/index.mjs"; +import { displayTestResults, hasFailures } from "../../../shared/dist/index.js"; const PORT = 8801; const MAX_RETRIES = 20; diff --git a/js/smoke/scenarios/cloudflare-worker-browser-no-compat/Makefile b/js/smoke/scenarios/cloudflare-worker-browser-no-compat/Makefile index a019fa3a3..a07495d3c 100644 --- a/js/smoke/scenarios/cloudflare-worker-browser-no-compat/Makefile +++ b/js/smoke/scenarios/cloudflare-worker-browser-no-compat/Makefile @@ -8,7 +8,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ [ "$$(basename $$f)" = "braintrust-latest.tgz" ] && continue; \ diff --git a/js/smoke/scenarios/cloudflare-worker-browser-no-compat/tests/worker.test.mjs b/js/smoke/scenarios/cloudflare-worker-browser-no-compat/tests/worker.test.mjs index 568bf0df7..d7f5df23b 100644 --- a/js/smoke/scenarios/cloudflare-worker-browser-no-compat/tests/worker.test.mjs +++ b/js/smoke/scenarios/cloudflare-worker-browser-no-compat/tests/worker.test.mjs @@ -1,8 +1,5 @@ import { spawn, execSync } from "node:child_process"; -import { - displayTestResults, - hasFailures, -} from "../../../shared/dist/index.mjs"; +import { displayTestResults, hasFailures } from "../../../shared/dist/index.js"; const PORT = 8800; const MAX_RETRIES = 20; diff --git a/js/smoke/scenarios/cloudflare-worker-node-compat/Makefile b/js/smoke/scenarios/cloudflare-worker-node-compat/Makefile index a03e18c1c..e66aac769 100644 --- a/js/smoke/scenarios/cloudflare-worker-node-compat/Makefile +++ b/js/smoke/scenarios/cloudflare-worker-node-compat/Makefile @@ -8,7 +8,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ [ "$$(basename $$f)" = "braintrust-latest.tgz" ] && continue; \ diff --git a/js/smoke/scenarios/cloudflare-worker-node-compat/tests/worker.test.mjs b/js/smoke/scenarios/cloudflare-worker-node-compat/tests/worker.test.mjs index 123f81e3c..3362712a8 100644 --- a/js/smoke/scenarios/cloudflare-worker-node-compat/tests/worker.test.mjs +++ b/js/smoke/scenarios/cloudflare-worker-node-compat/tests/worker.test.mjs @@ -1,8 +1,5 @@ import { spawn, execSync } from "node:child_process"; -import { - displayTestResults, - hasFailures, -} from "../../../shared/dist/index.mjs"; +import { displayTestResults, hasFailures } from "../../../shared/dist/index.js"; const PORT = 8799; const MAX_RETRIES = 20; diff --git a/js/smoke/scenarios/cloudflare-worker-node-no-compat/Makefile b/js/smoke/scenarios/cloudflare-worker-node-no-compat/Makefile index bf866c692..7fb8a9bc2 100644 --- a/js/smoke/scenarios/cloudflare-worker-node-no-compat/Makefile +++ b/js/smoke/scenarios/cloudflare-worker-node-no-compat/Makefile @@ -8,7 +8,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ [ "$$(basename $$f)" = "braintrust-latest.tgz" ] && continue; \ diff --git a/js/smoke/scenarios/cloudflare-worker-node-no-compat/tests/worker.test.mjs b/js/smoke/scenarios/cloudflare-worker-node-no-compat/tests/worker.test.mjs index 7150abd08..71ce8f789 100644 --- a/js/smoke/scenarios/cloudflare-worker-node-no-compat/tests/worker.test.mjs +++ b/js/smoke/scenarios/cloudflare-worker-node-no-compat/tests/worker.test.mjs @@ -1,8 +1,5 @@ import { spawn, execSync } from "node:child_process"; -import { - displayTestResults, - hasFailures, -} from "../../../shared/dist/index.mjs"; +import { displayTestResults, hasFailures } from "../../../shared/dist/index.js"; const PORT = 8802; const MAX_RETRIES = 20; diff --git a/js/smoke/scenarios/playwright-browser/Makefile b/js/smoke/scenarios/playwright-browser/Makefile index 98f43e1ff..f64fd9ff8 100644 --- a/js/smoke/scenarios/playwright-browser/Makefile +++ b/js/smoke/scenarios/playwright-browser/Makefile @@ -13,7 +13,7 @@ setup: echo "==> Using BRAINTRUST_TAR: $(BRAINTRUST_TAR)"; \ else \ echo "==> Building SDK"; \ - cd ../../.. && pnpm exec turbo build --filter=braintrust && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ + cd ../../.. && pnpm --filter=braintrust run build && mkdir -p artifacts && pnpm pack --pack-destination artifacts; \ \ for f in artifacts/braintrust-*.tgz; do \ [ "$$(basename $$f)" = "braintrust-latest.tgz" ] && continue; \ diff --git a/js/smoke/scenarios/playwright-browser/tests/browser.test.ts b/js/smoke/scenarios/playwright-browser/tests/browser.test.ts index 42dd1a9c5..3fbe0bfb8 100644 --- a/js/smoke/scenarios/playwright-browser/tests/browser.test.ts +++ b/js/smoke/scenarios/playwright-browser/tests/browser.test.ts @@ -2,7 +2,7 @@ import { test, expect } from "@playwright/test"; import { displayTestResults, type TestResult, -} from "../../../shared/dist/index.mjs"; +} from "../../../shared/dist/index.js"; test.describe("Braintrust SDK Browser Tests", () => { test("should run all browser tests (shared + eval + prompt)", async ({ diff --git a/js/smoke/shared/README.md b/js/smoke/shared/README.md index 608d19173..bc52b618d 100644 --- a/js/smoke/shared/README.md +++ b/js/smoke/shared/README.md @@ -17,8 +17,7 @@ This package provides reusable test logic that can be imported and run in differ The package is built with `tsup` to produce both CommonJS and ESM formats: -- `dist/index.js` - CommonJS build -- `dist/index.mjs` - ESM build +- `dist/index.js` - ESM build - `dist/index.d.ts` / `dist/index.d.mts` - TypeScript declarations This allows the same test code to be imported in both CJS and ESM environments. @@ -66,7 +65,7 @@ import { testBasicSpanLogging, testMultipleSpans, testNunjucksTemplate, -} from "../../shared/dist/index.mjs"; +} from "../../shared/dist/index.js"; import * as braintrust from "braintrust"; @@ -105,7 +104,7 @@ const { testMultipleSpans, } = require("../../shared"); -const braintrust = require("braintrust"); +import * as braintrust from "braintrust"; async function main() { const { all, passed, failed, xfail } = await runTests({ @@ -414,7 +413,7 @@ Tests basic eval functionality: All scenarios use `displayTestResults()` for consistent output: ```typescript -import { displayTestResults } from "../../shared/dist/index.mjs"; +import { displayTestResults } from "../../shared/dist/index.js"; // Called automatically by runTests(), but can also be used standalone displayTestResults({ diff --git a/js/smoke/shared/package.json b/js/smoke/shared/package.json index 1e9535fbf..523a19ffd 100644 --- a/js/smoke/shared/package.json +++ b/js/smoke/shared/package.json @@ -4,26 +4,26 @@ "private": true, "description": "Shared test utilities for Braintrust smoke tests", "main": "./dist/index.js", - "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" + "default": "./dist/index.js" } }, "files": [ "dist/**/*" ], "scripts": { - "build": "tsup", + "build": "vp pack", "clean": "rm -rf dist", - "watch": "tsup --watch" + "watch": "vp pack --watch" }, "devDependencies": { "@types/node": "^20.10.5", - "tsup": "^8.5.1", - "typescript": "^5.4.4" - } + "typescript": "^5.4.4", + "vite-plus": "1.1.0", + "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0" + }, + "type": "module" } diff --git a/js/smoke/shared/pnpm-lock.yaml b/js/smoke/shared/pnpm-lock.yaml index 0417dd584..356dc8f06 100644 --- a/js/smoke/shared/pnpm-lock.yaml +++ b/js/smoke/shared/pnpm-lock.yaml @@ -11,15 +11,33 @@ importers: '@types/node': specifier: ^20.10.5 version: 20.19.37 - tsup: - specifier: ^8.5.1 - version: 8.5.1(typescript@5.9.3) typescript: specifier: ^5.4.4 version: 5.9.3 + vite: + specifier: npm:@voidzero-dev/vite-plus-core@1.1.0 + version: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)' + vite-plus: + specifier: 1.1.0 + version: 1.1.0(@types/node@20.19.37)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.27.4)(typescript@5.9.3) packages: + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} + + '@blazediff/core@1.10.0': + resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==} + '@esbuild/aix-ppc64@0.27.4': resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} @@ -176,9 +194,6 @@ packages: cpu: [x64] os: [win32] - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -186,381 +201,1026 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + '@oxc-project/runtime@0.153.0': + resolution: {integrity: sha512-rXJvtlsVG4xJNq64hiaUTIatE7jbBgpa6l4QgmEa2jLgT3ULH9scUBkpsC9N1DfmqixLEyT6DuK6OSXozziIlw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.153.0': + resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} + + '@oxfmt/binding-android-arm-eabi@0.72.0': + resolution: {integrity: sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + '@oxfmt/binding-android-arm64@0.72.0': + resolution: {integrity: sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + '@oxfmt/binding-darwin-arm64@0.72.0': + resolution: {integrity: sha512-LjQ8tlevdtwraFlWC6AnQ1Forzde0clRmnsP3QmK+ydSElDt/J+5Jc+m+//Ly1G97aP7WCNUw110QMIUw2K7xA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + '@oxfmt/binding-darwin-x64@0.72.0': + resolution: {integrity: sha512-+ZbcxhB1C34RQR5IldSyY6Z/yalJIxHXhP36PGE4sEq0f/lgL5jCLmOKoDeVm8dqd9gFGDsd1bNWHSGCAwdSkw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + '@oxfmt/binding-freebsd-x64@0.72.0': + resolution: {integrity: sha512-yGPhA8sePEBFV9iGcprhyOk3Bz3fZshswMH3DpYIPZFLT1DG5LjiZ0j6AJQpSueaL6bUK0av1K+5+fe+ldy5EQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + '@oxfmt/binding-linux-arm-gnueabihf@0.72.0': + resolution: {integrity: sha512-/g5axuqFMcAtoQrFLR5EJEMDjR5Ea4GuzpdDmMpPWAgrF1DmL4kD1NPNVlpXEOxpFJnD6lPvLKYj1lzdlrfifA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + '@oxfmt/binding-linux-arm-musleabihf@0.72.0': + resolution: {integrity: sha512-InVitdHbS09PM+w+WinIiyX37rC9R3L7lUP9mKy9uKTEDSUFaLjWsLoreRIxKivnv7Ndqc6Trvi+K+b6ZzhiJg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + '@oxfmt/binding-linux-arm64-gnu@0.72.0': + resolution: {integrity: sha512-WKjlyRAxpPjXoqeuwFBim5LscgvY+RNEiZ971C9JlRQZONhJTsXDki/oHFYQE+fzzRpzXh3owZvOdT3kNL2g+g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + '@oxfmt/binding-linux-arm64-musl@0.72.0': + resolution: {integrity: sha512-poafnbsPpJhR+5Mbwpf8CscugTyiRAzjTOxRVn8ra3X2jnURs2xL5Du2xl5g2iW85lICMglpmLf8jDjZEbNgzQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + '@oxfmt/binding-linux-ppc64-gnu@0.72.0': + resolution: {integrity: sha512-zNUi0gKRsO7S3IfPzsTbnm88OedPuX32hmZFWZRWHvWJlAoZRwVbeO6Nb3hng4vDgL7qUKp4TGqzPz1mhsycYg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + '@oxfmt/binding-linux-riscv64-gnu@0.72.0': + resolution: {integrity: sha512-qTY7z/iyF/rWI2HWwWMevXqZW5PZUOofUy2afK7v6rseXnpld3yjgyIk6pCux3DqWQ0lB+f5atvLJNBwzLvFsw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + '@oxfmt/binding-linux-riscv64-musl@0.72.0': + resolution: {integrity: sha512-w18qfo8vEMD6EKEHtzTvjc6jH8yCyXIT1Bn7PFWJd0PpZyzH/VQDx4xvjCh0LGz6GRzqiezbisTdG9AoXtpkZQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + '@oxfmt/binding-linux-s390x-gnu@0.72.0': + resolution: {integrity: sha512-grW8HwEir+vK7fCcq1ZgL3NHQCfZ1PidiERE9Hbf+24xGys+DoDaoqTht4ccfn0PcBwbViLkwkda/KNpJu2KRQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + '@oxfmt/binding-linux-x64-gnu@0.72.0': + resolution: {integrity: sha512-wQJRQfWBRIn88Pkr449EUViwRayrYUScg7ty+Ihiem21mQ9BUA6S9e/4RAgyNk9lMag0KBouyKskjq61rOC0WQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + '@oxfmt/binding-linux-x64-musl@0.72.0': + resolution: {integrity: sha512-T1B0uQzafxuGPjjaPrZJ497nxoi1BiI65JfsYc56nNpmev3msfb/iaTmH2kQuNFqa8PKxpMlf3GhWrWBvBPORQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + '@oxfmt/binding-openharmony-arm64@0.72.0': + resolution: {integrity: sha512-OKxxgTurl7+hypVplO8QRAbbQlwEs0j1Y6FZv+ZN2qNbdxQk2muMlzuJNBGTCWwxQ0rkkOD1uK/DW1fvzoUSJA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + '@oxfmt/binding-win32-arm64-msvc@0.72.0': + resolution: {integrity: sha512-L78TKzURqvjuZOCo88ixMbPXf/RP7u31Y0cmjJ/KEmrHmfYhmhEvsDaTYPiyTRne64jUgCbADfY0YeoQk4/Xyw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + '@oxfmt/binding-win32-ia32-msvc@0.72.0': + resolution: {integrity: sha512-gQtFW+Ii8BWgtJTAD17TY/WsdNS5gUfAPov8iI1jeq4r9lRRYImmxPL08UU00/RLaCfBJwh2UgINnDY0iOiZ+A==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + '@oxfmt/binding-win32-x64-msvc@0.72.0': + resolution: {integrity: sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@7.0.2003': + resolution: {integrity: sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@7.0.2003': + resolution: {integrity: sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@7.0.2003': + resolution: {integrity: sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@7.0.2003': + resolution: {integrity: sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==} cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@7.0.2003': + resolution: {integrity: sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==} + cpu: [arm64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + '@oxlint-tsgolint/win32-x64@7.0.2003': + resolution: {integrity: sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==} cpu: [x64] os: [win32] - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@oxlint/binding-android-arm-eabi@1.87.0': + resolution: {integrity: sha512-22tVIHlB9898OYsBKmYB6He1MCSqdTmK1BVWuSNUTVrpS0KWNege+U9uEYqGJ+KatQag0r/oU44i3OOs46z7HQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] - '@types/node@20.19.37': - resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + '@oxlint/binding-android-arm64@1.87.0': + resolution: {integrity: sha512-JfZQ3MWf74n8ov0DojZpkOduIubYJvZL5tbTMMhDutsal6u1xH1QuNpN+lK+EQwF8AeAfgwTR2fgh5U2br27XA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true + '@oxlint/binding-darwin-arm64@1.87.0': + resolution: {integrity: sha512-rkx6Xvvw93z2GImVOyvbZwAZcoY0Z6MJW177XNsSB+i/CBsH7ANyelTp01VZyIkM3iSXtqKVb4+ptBgPHap5pQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.87.0': + resolution: {integrity: sha512-cw5nYw7L/pQ2SgGBgOcg/Qo89llQre3vIc/kuBLhSICzu1r5s8L76H3ug6AIFsD98WpROj8/z5JVzkf4daxHaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + '@oxlint/binding-freebsd-x64@1.87.0': + resolution: {integrity: sha512-x/SuznE6loRZK9gz9AS4ZmzNzSu+5KdeoCS+sVHhnrE8HM6AD8UyTjwldQTTqlXGvkwHASgB0NiTNOc3o3PdAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' + '@oxlint/binding-linux-arm-gnueabihf@1.87.0': + resolution: {integrity: sha512-+JEDmCAXiiFmKhAjZhgAOwaHWY6WENOQ5QA+lFuY59tgsUCDOTRi46B+lENK6CWvtxsXpwkZXNOgpjAQcpIpnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + '@oxlint/binding-linux-arm-musleabihf@1.87.0': + resolution: {integrity: sha512-/uC9XqXLAC9AHFLNycwmSQEvKqGrOCgU+uXALI7gNW0DG81TJ/3T2eHnKdRvcb5R1gFEfjxDwI4e4zIa29rICQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} + '@oxlint/binding-linux-arm64-gnu@1.87.0': + resolution: {integrity: sha512-FTRYPsBy/M728vnUv7l+KAwSAa3Qi4BQbhVvy7NYuMTZ0sUa/OWsbMx79HVFw6u/5KWcaYayDsoOiv7xbevzDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} + '@oxlint/binding-linux-arm64-musl@1.87.0': + resolution: {integrity: sha512-6sC+N7IY0h1egd8SrYUGNN7CUyKJZ4bIZakKa7V7hfinf4jfi8Kq4+klJvFp5DqXcwMn44I5MeBwmJluO1odFg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + '@oxlint/binding-linux-ppc64-gnu@1.87.0': + resolution: {integrity: sha512-d5Mnn+ICbhoJPi37Ccjy+86T5qfH2+mSTrTMBXNPfB+wqSWOayBBLLIrCXdUy1YssluCVdGkl+DO9w5r5ODmUA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} + '@oxlint/binding-linux-riscv64-gnu@1.87.0': + resolution: {integrity: sha512-sf5YWuUo1Dx9loqWCgF8P278MycfcD4JHjDC/LV5j11L3Lqa4PFiwCjD6B8kP4AjsgZZkLn3xbpJEtZBaccs4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + '@oxlint/binding-linux-riscv64-musl@1.87.0': + resolution: {integrity: sha512-+aCraYbmK3uU3P0rFojjeaK9pGY6JvBDXh/0CM+gPyWKmNmXmnFe0qj4qj02U4ibFpbd9Pa9CmDvIdjJUd29rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} - engines: {node: '>=18'} - hasBin: true + '@oxlint/binding-linux-s390x-gnu@1.87.0': + resolution: {integrity: sha512-7rzbZQIljo6VLWVPqUyLa2P/WV3VFsJB4M52R4NCUJn1A50yjtUsHxiMHFL9opEKGFIiTbZ3VQzmtcJOPUq4NQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true + '@oxlint/binding-linux-x64-gnu@1.87.0': + resolution: {integrity: sha512-Dry46Fa+AXE/xt9J/YriujU1pU9mzfNWHGQwtOYibAFH8HLrKWCi93AhIGnuUMiwn2JbuBkRyz51f50Ly4r4ow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + '@oxlint/binding-linux-x64-musl@1.87.0': + resolution: {integrity: sha512-f/HMZez74gJvnRpGz+b5sdGyWrEgRLmFTXg9sNoqjPWogGxYHUhXKTKyMiYsu9rUGR2ciYqL3fk+Ua4JhMJbRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] + '@oxlint/binding-openharmony-arm64@1.87.0': + resolution: {integrity: sha512-lEC6ZfPZm/07JlB7FsOWX9o1RiHlXiBW594LSUGuXmekC8LZmvUPP9XzE2WZcNxJi5cHA/Fw56o02c3JxgMA5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} + '@oxlint/binding-win32-arm64-msvc@1.87.0': + resolution: {integrity: sha512-afBWKOD36dUiA9AenaiWpCg8xfyUVPYznyAAVXu413brK4RA5UI/Xoc4yfeGIT8G1a44MV+JR7hioryerBDzGQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} + '@oxlint/binding-win32-ia32-msvc@1.87.0': + resolution: {integrity: sha512-daptNsEOuru4/nJTxV4DBHNJ5GepzmaBVQf+jjgKQGi9NRlZKik+RFq4hUL7ewsF1pwjKM+SkYaMbF82UwFGnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + '@oxlint/binding-win32-x64-msvc@1.87.0': + resolution: {integrity: sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@oxlint/plugins@1.79.0': + resolution: {integrity: sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + '@testing-library/user-event@14.6.8': + resolution: {integrity: sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} + '@types/node@20.19.37': + resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + + '@vitest/browser-preview@5.0.3': + resolution: {integrity: sha512-jfnEBv5yDRPR1MeiTlTW5bcZoPsiI7MBhpZR8IJiSxVhdK6Pa4gPk3tLHTRxVHy24kkWxY2TnUbnszU3J1bQJw==} + peerDependencies: + vitest: 5.0.3 + + '@vitest/browser@5.0.3': + resolution: {integrity: sha512-7KHIkVA/gTRxQV9qPa9qLK+g5INZG6iRrlwXSFG5EKsOomW3UzzW7wsdt1Yp5PufzQ43lniuONlb4lUVCC2qSg==} + peerDependencies: + vitest: 5.0.3 + + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@5.0.3': + resolution: {integrity: sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==} + + '@vitest/snapshot@5.0.3': + resolution: {integrity: sha512-SK4YTg9jL+jwakHIZxfciuvyOQsbY15AueR3USGtsXzmy+3FbJgqjAbC+ac20mRLwfEDtO5sfTqK6bUCDTRVPA==} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + '@vitest/ui@5.0.3': + resolution: {integrity: sha512-9dfvqDBvSbHM6y9YVXxAypkl2z7mrWozK/JFo237qI0cj/3M/kzDKiVZqzi3TE54Ler7Jditud7OMBZCyUvRHA==} + peerDependencies: + vitest: 5.0.3 + + '@vitest/utils@5.0.3': + resolution: {integrity: sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==} - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} + '@voidzero-dev/vite-plus-core@1.1.0': + resolution: {integrity: sha512-Ai6HQP1zG0H3ZrmStEaMVKtDzW0xU3VgMzm3LFaXDQMEp8nAqvp0VGoU8HmpB9xv9vDSl+UxiSNOpuY9T714OQ==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@types/node': ^20.19.0 || >=22.12.0 + '@typescript/native-preview': '*' + '@vitejs/devtools': ^0.7.1 + '@volar/typescript': ~2.4.0 + '@vue/language-core': ~3.2.0 || ~3.3.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' - postcss: '>=8.0.9' + less: ^4.0.0 + publint: ^0.3.8 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 tsx: ^4.8.1 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: '>=0.5.0' + unrun: '*' + vue-tsc: ~3.2.0 || ~3.3.0 yaml: ^2.4.2 peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@types/node': + optional: true + '@typescript/native-preview': + optional: true + '@vitejs/devtools': + optional: true + '@volar/typescript': + optional: true + '@vue/language-core': + optional: true + esbuild: + optional: true jiti: optional: true - postcss: + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: optional: true tsx: optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + vue-tsc: + optional: true yaml: optional: true - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} + '@voidzero-dev/vite-plus-darwin-arm64@1.1.0': + resolution: {integrity: sha512-iWJdQZBRZWB6X1Aadk0Rrs/NJeP32Qb0OGeWnu1AYL0g6ZCj1yy3l9Oa+HasDyG97s+4UYo523LdBAAv3dlFwg==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [darwin] - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} + '@voidzero-dev/vite-plus-darwin-x64@1.1.0': + resolution: {integrity: sha512-IYSQ2KdigRMuVkKCC+e5uNliCTSHa8j3ajHsFG2cfXoiGHS1f4ausbMEzNYtn9Tcn0Xsyo7l0LN8lvB7MapJhA==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-PmrJG8Md5KCWi/akWJz5GRjei2QfRf48R4rPB9CfualzAAlpMOmaMGeV3eloWVAFye85uhdtzCmIAqN5OUVBJA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} + '@voidzero-dev/vite-plus-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-zFr57++m5U474XEMcFGV025TOv9V2I5d87yylcuI/SvxQwDE9P7T5iRl55vGzQOzzInSnQuru6ByNIfmGa+84A==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true + '@voidzero-dev/vite-plus-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-DXeRuTMylEbIVCQX+pSBM27lSPQwAKO6PDnGBJ6nFGhu37AmNeVl7cIChtcniXOuykbFduhSt3eIr/6CZHoEag==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} + '@voidzero-dev/vite-plus-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-J7/Mb/kxtENUsL5Gh+8SSr/p054fSqU4oj0GEthr/2VEFSwZouOUigk6CzE7ANKFeTNJm40n8QCnjm7Oiy09pQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-EXWz8Tt7skmI0fLMSq5zYcuHo6SCF0reG55qDB/NMvWVIAca6Dborq7D1HyKA8v6p39zOl8r07gR7yXuYv7eeQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [win32] - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + '@voidzero-dev/vite-plus-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-K9k77Mj/TszFHKsZePKsyfsIPo8HbjKeJUxaSJ6YZKAmSh9QvDsMFe4i4/HALwyii/PR1EQ8N0iN1nf9cNimGQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} + '@yuku-codegen/binding-android-arm64@0.9.5': + resolution: {integrity: sha512-jrOY5WM+AaAqkv51fHP1x28ifto4WgcZVzodLUyMU1jMWn5Sq+VciRdk/n8E0Ey5w4p4cWWE+m5GfXWOYh7Kzw==} + cpu: [arm64] + os: [android] + deprecated: yuku-codegen is pure JavaScript since 0.14 - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true + '@yuku-codegen/binding-darwin-arm64@0.9.5': + resolution: {integrity: sha512-4O4lkCQIzPZGjiNB1GORSI6hBECbiiSBS/APZXPvNKYH+nhY4uuqv03LNXA+SET3hoBjvr95P5rIhY8KQaQUBA==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + '@yuku-codegen/binding-darwin-x64@0.9.5': + resolution: {integrity: sha512-9EvoUO0SEhD6/d8VHdWuPerepPMSR1y84+UEgz8Un1Ope14Oe7xMvePpEQLTLovoIFZ8Zg3iZ8z8E11pZMqC3g==} + cpu: [x64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true + '@yuku-codegen/binding-freebsd-x64@0.9.5': + resolution: {integrity: sha512-HqT78WwgHTmp8lwujoUa9CrIortX4DdpuiVC18ZPSGvuuJf4ylpIEI6QrQEM78Zwz3muhAWAOXZ5irdiYY+AyA==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-codegen is pure JavaScript since 0.14 - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': + resolution: {integrity: sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + '@yuku-codegen/binding-linux-arm-musl@0.9.5': + resolution: {integrity: sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 -snapshots: + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 - '@esbuild/aix-ppc64@0.27.4': - optional: true + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 - '@esbuild/android-arm64@0.27.4': - optional: true + '@yuku-codegen/binding-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 - '@esbuild/android-arm@0.27.4': - optional: true + '@yuku-codegen/binding-win32-arm64@0.9.5': + resolution: {integrity: sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w==} + cpu: [arm64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 - '@esbuild/android-x64@0.27.4': - optional: true + '@yuku-codegen/binding-win32-x64@0.9.5': + resolution: {integrity: sha512-FxENahEjWSan59Syh/us/Kf4wNq8qrJLFJ3R2N4Oiwtb6yNdz/rWLNTIoNSssnsp7IoWJdUP6o/Z8ppg7lXMcg==} + cpu: [x64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 - '@esbuild/darwin-arm64@0.27.4': - optional: true + '@yuku-parser/binding-android-arm64@0.9.5': + resolution: {integrity: sha512-A2JCFCSHfnficqYEw4Iujpx7XrkMM3UfFcgJFmYpZSo9zM4nyhmdIIE0FogSduuU60lhM0/UcfuUBXIVNBMlGQ==} + cpu: [arm64] + os: [android] + deprecated: yuku-parser runs on yuku-core since 0.14 - '@esbuild/darwin-x64@0.27.4': - optional: true + '@yuku-parser/binding-darwin-arm64@0.9.5': + resolution: {integrity: sha512-3PiyU+Eare4YuKaQ22N98/yAiROPY5o/NQJHraICzDvk4pgS+m+bgLKOvkGBRn33OnV95Vdv1Mn38b+MQHpULQ==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 - '@esbuild/freebsd-arm64@0.27.4': - optional: true + '@yuku-parser/binding-darwin-x64@0.9.5': + resolution: {integrity: sha512-blFMAFI7AInI83XaiOF8cIeiRM46Nz9EfpZtZPRLbKxSA9aAr5v8aHYpmfN6NoyXxAv/10pwS4gsAuc1W6fy5Q==} + cpu: [x64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 - '@esbuild/freebsd-x64@0.27.4': - optional: true + '@yuku-parser/binding-freebsd-x64@0.9.5': + resolution: {integrity: sha512-TsNuL4qsZdO0tHp5GW43Y5fHeLPpPHA675wdcPNTcdq2ZO5AvsQWFFoiDg8CH4oBktfsQ9tdvKhjLnLYwKvp4g==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-gnu@0.9.5': + resolution: {integrity: sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-musl@0.9.5': + resolution: {integrity: sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-win32-arm64@0.9.5': + resolution: {integrity: sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg==} + cpu: [arm64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-win32-x64@0.9.5': + resolution: {integrity: sha512-FrERt9YWatY3bJfSUdi4YWY+6iQcyo3MzCC821BxlWM5TFZEHijnpz/bknR79VHlXY/9CvXz8ZlaAGPsTtN3nw==} + cpu: [x64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-toolchain/types@0.9.5': + resolution: {integrity: sha512-KiuLNNgX9uNealaWAR+G3/cMXnRk9x4TY2EkYe/KIag+UPdwiA0RRf1hr1WAxzTP8KGzCTkqUdLPvqh32sEO3w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + oxfmt@0.72.0: + resolution: {integrity: sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@7.0.2003: + resolution: {integrity: sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==} + hasBin: true + + oxlint@1.87.0: + resolution: {integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.30: + resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.2.0: + resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} + engines: {node: ^20.0.0 || >=22.0.0} + + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} + engines: {node: '>=14.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite-plus@1.1.0: + resolution: {integrity: sha512-khkRlY70qqbBV0QmF9QKXSrhxIRYftb1H05AJMowMoq7iouchmAoXYeReZ8pKKMOadqrWVxFa3S9BA2m7w59iA==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + playwright: '*' + peerDependenciesMeta: + '@vitest/browser-playwright': + optional: true + '@vitest/browser-webdriverio': + optional: true + playwright: + optional: true + + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + 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 + + yuku-ast@0.9.5: + resolution: {integrity: sha512-Q8qW8WwQnN5Cm0ZZivdRIfv0sRLTjUq0YumXJkw8CYN1aCdICH9rk4C47/4n6kA5EqDtlj+F608twoTSV2MwdQ==} + + yuku-codegen@0.9.5: + resolution: {integrity: sha512-zGUVyDpSK4b6c9B0yyxi2P0wujn1XZTbG9dCb7d6gyAoP63EMgyLzUwPUvwxPG5jgYqhlI7w+S3oCyapqGr4PA==} + + yuku-parser@0.9.5: + resolution: {integrity: sha512-IBnAdNVMswWbJcM7woPluOudectJzFjJ1N8jVRxP9Uq4CIv5hZdBIcdmtRbFDYF/Ph2j2gVup4YJ4N9mh0jYFA==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/runtime@7.29.10': {} + + '@blazediff/core@1.10.0': {} + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true '@esbuild/linux-arm64@0.27.4': optional: true @@ -616,125 +1276,396 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@rollup/rollup-android-arm-eabi@4.60.1': + '@oxc-project/runtime@0.153.0': {} + + '@oxc-project/types@0.153.0': {} + + '@oxfmt/binding-android-arm-eabi@0.72.0': + optional: true + + '@oxfmt/binding-android-arm64@0.72.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.72.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.72.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.72.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.72.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.72.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.72.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.72.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.72.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.72.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.72.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.72.0': optional: true - '@rollup/rollup-android-arm64@4.60.1': + '@oxfmt/binding-win32-x64-msvc@0.72.0': optional: true - '@rollup/rollup-darwin-arm64@4.60.1': + '@oxlint-tsgolint/darwin-arm64@7.0.2003': optional: true - '@rollup/rollup-darwin-x64@4.60.1': + '@oxlint-tsgolint/darwin-x64@7.0.2003': optional: true - '@rollup/rollup-freebsd-arm64@4.60.1': + '@oxlint-tsgolint/linux-arm64@7.0.2003': optional: true - '@rollup/rollup-freebsd-x64@4.60.1': + '@oxlint-tsgolint/linux-x64@7.0.2003': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + '@oxlint-tsgolint/win32-arm64@7.0.2003': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.1': + '@oxlint-tsgolint/win32-x64@7.0.2003': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.1': + '@oxlint/binding-android-arm-eabi@1.87.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.1': + '@oxlint/binding-android-arm64@1.87.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.1': + '@oxlint/binding-darwin-arm64@1.87.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.1': + '@oxlint/binding-darwin-x64@1.87.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.1': + '@oxlint/binding-freebsd-x64@1.87.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.1': + '@oxlint/binding-linux-arm-gnueabihf@1.87.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.1': + '@oxlint/binding-linux-arm-musleabihf@1.87.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.1': + '@oxlint/binding-linux-arm64-gnu@1.87.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.1': + '@oxlint/binding-linux-arm64-musl@1.87.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.1': + '@oxlint/binding-linux-ppc64-gnu@1.87.0': optional: true - '@rollup/rollup-linux-x64-musl@4.60.1': + '@oxlint/binding-linux-riscv64-gnu@1.87.0': optional: true - '@rollup/rollup-openbsd-x64@4.60.1': + '@oxlint/binding-linux-riscv64-musl@1.87.0': optional: true - '@rollup/rollup-openharmony-arm64@4.60.1': + '@oxlint/binding-linux-s390x-gnu@1.87.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.1': + '@oxlint/binding-linux-x64-gnu@1.87.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.1': + '@oxlint/binding-linux-x64-musl@1.87.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.1': + '@oxlint/binding-openharmony-arm64@1.87.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.1': + '@oxlint/binding-win32-arm64-msvc@1.87.0': optional: true + '@oxlint/binding-win32-ia32-msvc@1.87.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.87.0': + optional: true + + '@oxlint/plugins@1.79.0': {} + + '@polka/url@1.0.0-next.29': {} + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.10 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/user-event@14.6.8(@testing-library/dom@10.4.2)': + dependencies: + '@testing-library/dom': 10.4.2 + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/node@20.19.37': dependencies: undici-types: 6.21.0 - acorn@8.16.0: {} + '@vitest/browser-preview@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3))(vitest@5.0.3)': + dependencies: + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.8(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3))(vitest@5.0.3) + vitest: 5.0.3(@types/node@20.19.37)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite - any-promise@1.3.0: {} + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.2.0 + vitest: 5.0.3(@types/node@20.19.37)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)) + ws: 8.22.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite - bundle-require@5.1.0(esbuild@0.27.4): + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3))': dependencies: - esbuild: 0.27.4 - load-tsconfig: 0.2.5 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)' - cac@6.7.14: {} + '@vitest/pretty-format@5.0.3': + dependencies: + tinyrainbow: 3.2.0 - chokidar@4.0.3: + '@vitest/snapshot@5.0.3': dependencies: - readdirp: 4.1.2 + '@vitest/pretty-format': 5.0.3 + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pathe: 2.0.3 - commander@4.1.1: {} + '@vitest/spy@5.0.3': {} - confbox@0.1.8: {} + '@vitest/ui@5.0.3(vitest@5.0.3)': + dependencies: + '@vitest/utils': 5.0.3 + fflate: 0.8.3 + flatted: 3.4.4 + pathe: 2.0.3 + sirv: 3.0.2 + tinyrainbow: 3.2.0 + vitest: 5.0.3(@types/node@20.19.37)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)) - consola@3.4.2: {} + '@vitest/utils@5.0.3': + dependencies: + '@vitest/pretty-format': 5.0.3 + convert-source-map: 2.0.0 + tinyrainbow: 3.2.0 - debug@4.4.3: + '@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)': dependencies: - ms: 2.1.3 + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.30 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@types/node': 20.19.37 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.27.4 + fsevents: 2.3.3 + typescript: 5.9.3 + + '@voidzero-dev/vite-plus-darwin-arm64@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-darwin-x64@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-musl@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-gnu@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-win32-x64-msvc@1.1.0': + optional: true + + '@yuku-codegen/binding-android-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.9.5': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-win32-x64@0.9.5': + optional: true + + '@yuku-parser/binding-android-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-darwin-x64@0.9.5': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.9.5': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.9.5': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.9.5': + optional: true + + '@yuku-parser/binding-win32-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-win32-x64@0.9.5': + optional: true + + '@yuku-toolchain/types@0.9.5': {} + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + assertion-error@2.0.1: {} + + chai@6.3.0: {} + + convert-source-map@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + dom-accessibility-api@0.5.16: {} + + es-module-lexer@2.3.2: {} esbuild@0.27.4: optionalDependencies: @@ -764,162 +1695,318 @@ snapshots: '@esbuild/win32-arm64': 0.27.4 '@esbuild/win32-ia32': 0.27.4 '@esbuild/win32-x64': 0.27.4 + optional: true + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 - fdir@6.5.0(picomatch@4.0.4): + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.7 - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.2 - rollup: 4.60.1 + fflate@0.8.3: {} + + flatted@3.4.4: {} fsevents@2.3.3: optional: true - joycon@3.1.1: {} + js-tokens@4.0.0: {} - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} + lightningcss-android-arm64@1.33.0: + optional: true - load-tsconfig@0.2.5: {} + lightningcss-darwin-arm64@1.33.0: + optional: true - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + lightningcss-darwin-x64@1.33.0: + optional: true - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 + lightningcss-freebsd-x64@1.33.0: + optional: true - ms@2.1.3: {} + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 + lightningcss-linux-arm64-gnu@1.33.0: + optional: true - object-assign@4.1.1: {} + lightningcss-linux-arm64-musl@1.33.0: + optional: true - pathe@2.0.3: {} + lightningcss-linux-x64-gnu@1.33.0: + optional: true - picocolors@1.1.1: {} + lightningcss-linux-x64-musl@1.33.0: + optional: true - picomatch@4.0.4: {} + lightningcss-win32-arm64-msvc@1.33.0: + optional: true - pirates@4.0.7: {} + lightningcss-win32-x64-msvc@1.33.0: + optional: true - pkg-types@1.3.1: + lightningcss@1.33.0: dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - postcss-load-config@6.0.1: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lz-string@1.5.0: {} + + magic-string@1.4.3: dependencies: - lilconfig: 3.1.3 + '@jridgewell/sourcemap-codec': 1.6.0 + + mrmime@2.0.1: {} - readdirp@4.1.2: {} + nanoid@3.3.20: {} - resolve-from@5.0.0: {} + obug@2.2.1: {} - rollup@4.60.1: + oxfmt@0.72.0(vite-plus@1.1.0(@types/node@20.19.37)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.27.4)(typescript@5.9.3)): dependencies: - '@types/estree': 1.0.8 + tinypool: 2.2.0 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 - fsevents: 2.3.3 + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@types/node@20.19.37)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.27.4)(typescript@5.9.3) + + oxlint-tsgolint@7.0.2003: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 7.0.2003 + '@oxlint-tsgolint/darwin-x64': 7.0.2003 + '@oxlint-tsgolint/linux-arm64': 7.0.2003 + '@oxlint-tsgolint/linux-x64': 7.0.2003 + '@oxlint-tsgolint/win32-arm64': 7.0.2003 + '@oxlint-tsgolint/win32-x64': 7.0.2003 + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@types/node@20.19.37)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.27.4)(typescript@5.9.3)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@types/node@20.19.37)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.27.4)(typescript@5.9.3) + + pathe@2.0.3: {} + + picocolors@1.1.1: {} - source-map@0.7.6: {} + picomatch@4.0.7: {} - sucrase@3.35.1: + pngjs@7.0.0: {} + + postcss@8.5.30: dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.15 - ts-interface-checker: 0.1.13 - - thenify-all@1.6.0: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + pretty-format@27.5.1: dependencies: - thenify: 3.3.1 + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + react-is@17.0.2: {} - thenify@3.3.1: + sirv@3.0.2: dependencies: - any-promise: 1.3.0 + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.2: {} + + std-env@4.3.0: {} + + tinybench@6.2.2: {} - tinyexec@0.3.2: {} + tinyexec@1.3.1: {} - tinyglobby@0.2.15: + tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 - tree-kill@1.2.2: {} + tinypool@2.2.0: {} - ts-interface-checker@0.1.13: {} + tinyrainbow@3.2.0: {} - tsup@8.5.1(typescript@5.9.3): + totalist@3.0.1: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + vite-plus@1.1.0(@types/node@20.19.37)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.27.4)(typescript@5.9.3): dependencies: - bundle-require: 5.1.0(esbuild@0.27.4) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.4 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1 - resolve-from: 5.0.0 - rollup: 4.60.1 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@types/node@20.19.37)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.27.4)(typescript@5.9.3)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@types/node@20.19.37)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.27.4)(typescript@5.9.3)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)' + vitest: 5.0.3(@types/node@20.19.37)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)) optionalDependencies: - typescript: 5.9.3 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom - jiti - - supports-color + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc - yaml - typescript@5.9.3: {} + vitest@5.0.3(@types/node@20.19.37)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3)' + why-is-node-running: 3.2.1 + optionalDependencies: + '@types/node': 20.19.37 + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@20.19.37)(esbuild@0.27.4)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw - ufo@1.6.3: {} + why-is-node-running@3.2.1: {} - undici-types@6.21.0: {} + ws@8.22.0: {} + + yuku-ast@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + + yuku-codegen@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.9.5 + '@yuku-codegen/binding-darwin-arm64': 0.9.5 + '@yuku-codegen/binding-darwin-x64': 0.9.5 + '@yuku-codegen/binding-freebsd-x64': 0.9.5 + '@yuku-codegen/binding-linux-arm-gnu': 0.9.5 + '@yuku-codegen/binding-linux-arm-musl': 0.9.5 + '@yuku-codegen/binding-linux-arm64-gnu': 0.9.5 + '@yuku-codegen/binding-linux-arm64-musl': 0.9.5 + '@yuku-codegen/binding-linux-x64-gnu': 0.9.5 + '@yuku-codegen/binding-linux-x64-musl': 0.9.5 + '@yuku-codegen/binding-win32-arm64': 0.9.5 + '@yuku-codegen/binding-win32-x64': 0.9.5 + + yuku-parser@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + yuku-ast: 0.9.5 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.9.5 + '@yuku-parser/binding-darwin-arm64': 0.9.5 + '@yuku-parser/binding-darwin-x64': 0.9.5 + '@yuku-parser/binding-freebsd-x64': 0.9.5 + '@yuku-parser/binding-linux-arm-gnu': 0.9.5 + '@yuku-parser/binding-linux-arm-musl': 0.9.5 + '@yuku-parser/binding-linux-arm64-gnu': 0.9.5 + '@yuku-parser/binding-linux-arm64-musl': 0.9.5 + '@yuku-parser/binding-linux-x64-gnu': 0.9.5 + '@yuku-parser/binding-linux-x64-musl': 0.9.5 + '@yuku-parser/binding-win32-arm64': 0.9.5 + '@yuku-parser/binding-win32-x64': 0.9.5 diff --git a/js/smoke/shared/src/suites/basic-logging.ts b/js/smoke/shared/src/suites/basic-logging.ts index 1a92af9d4..0103bb74d 100644 --- a/js/smoke/shared/src/suites/basic-logging.ts +++ b/js/smoke/shared/src/suites/basic-logging.ts @@ -5,7 +5,7 @@ import type { LoggerInstance } from "../helpers/types"; import { assert, assertEqual, assertNotEmpty } from "../helpers/assertions"; -import { register } from "../helpers/register"; +import { register, type TestFn } from "../helpers/register"; const PROJECT_ID = "test-project-id"; @@ -14,7 +14,7 @@ type InitLoggerFn = (options: { projectId?: string; }) => LoggerInstance; -export const testBasicSpanLogging = register( +export const testBasicSpanLogging: TestFn = register( "testBasicSpanLogging", async (braintrust, { backgroundLogger }) => { const initLogger = braintrust.initLogger as InitLoggerFn; @@ -50,7 +50,7 @@ export const testBasicSpanLogging = register( { requiresTestHarness: true }, ); -export const testMultipleSpans = register( +export const testMultipleSpans: TestFn = register( "testMultipleSpans", async (braintrust, { backgroundLogger }) => { const initLogger = braintrust.initLogger as InitLoggerFn; @@ -82,7 +82,7 @@ export const testMultipleSpans = register( { requiresTestHarness: true }, ); -export const testDirectLogging = register( +export const testDirectLogging: TestFn = register( "testDirectLogging", async (braintrust, { backgroundLogger }) => { const initLogger = braintrust.initLogger as InitLoggerFn; @@ -107,7 +107,7 @@ export const testDirectLogging = register( { requiresTestHarness: true }, ); -export const testJSONAttachment = register( +export const testJSONAttachment: TestFn = register( "testJSONAttachment", async (braintrust, { backgroundLogger }) => { const initLogger = braintrust.initLogger as InitLoggerFn; @@ -165,7 +165,7 @@ export const testJSONAttachment = register( { requiresTestHarness: true }, ); -export const testAsyncLocalStorageTraced = register( +export const testAsyncLocalStorageTraced: TestFn = register( "testAsyncLocalStorageTraced", async (braintrust, { backgroundLogger }) => { const initLogger = braintrust.initLogger as InitLoggerFn; @@ -228,7 +228,7 @@ export const testAsyncLocalStorageTraced = register( { requiresTestHarness: true }, ); -export const testNestedTraced = register( +export const testNestedTraced: TestFn = register( "testNestedTraced", async (braintrust, { backgroundLogger }) => { const initLogger = braintrust.initLogger as InitLoggerFn; @@ -309,7 +309,7 @@ export const testNestedTraced = register( { requiresTestHarness: true }, ); -export const testCurrentSpan = register( +export const testCurrentSpan: TestFn = register( "testCurrentSpan", async (braintrust, { backgroundLogger }) => { const initLogger = braintrust.initLogger as InitLoggerFn; diff --git a/js/smoke/shared/src/suites/eval-smoke.ts b/js/smoke/shared/src/suites/eval-smoke.ts index ae9e89dcd..9cd559830 100644 --- a/js/smoke/shared/src/suites/eval-smoke.ts +++ b/js/smoke/shared/src/suites/eval-smoke.ts @@ -9,7 +9,7 @@ import { assertHasProperty, assertType, } from "../helpers/assertions"; -import { register } from "../helpers/register"; +import { register, type TestFn } from "../helpers/register"; function exactMatchScore({ output, @@ -21,77 +21,80 @@ function exactMatchScore({ return { name: "exact_match", score: output === expected ? 1 : 0 }; } -export const testEvalSmoke = register("testEvalSmoke", async (braintrust) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const Eval = braintrust.Eval as any; +export const testEvalSmoke: TestFn = register( + "testEvalSmoke", + async (braintrust) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Eval = braintrust.Eval as any; - assertDefined(Eval, "Eval must exist"); - assertType(Eval, "function", "Eval must be a function"); + assertDefined(Eval, "Eval must exist"); + assertType(Eval, "function", "Eval must be a function"); - const evalData = [ - { input: "Alice", expected: "Hi Alice" }, - { input: "Bob", expected: "Hi Bob" }, - { input: "Charlie", expected: "Hi Charlie" }, - ]; + const evalData = [ + { input: "Alice", expected: "Hi Alice" }, + { input: "Bob", expected: "Hi Bob" }, + { input: "Charlie", expected: "Hi Charlie" }, + ]; - const result = await Eval( - "smoke-eval-test", - { - data: evalData, - task: async (input: string) => `Hi ${input}`, - scores: [exactMatchScore], - }, - { - noSendLogs: true, - returnResults: true, - }, - ); - - assertDefined(result, "Eval should return a result"); - const r = result as Record; + const result = await Eval( + "smoke-eval-test", + { + data: evalData, + task: async (input: string) => `Hi ${input}`, + scores: [exactMatchScore], + }, + { + noSendLogs: true, + returnResults: true, + }, + ); - assertHasProperty(r, "summary", "Eval result missing summary"); - assertHasProperty(r, "results", "Eval result missing results"); + assertDefined(result, "Eval should return a result"); + const r = result as Record; - const summary = r.summary as Record; - const results = r.results as Array>; + assertHasProperty(r, "summary", "Eval result missing summary"); + assertHasProperty(r, "results", "Eval result missing results"); - assertEqual( - results.length, - evalData.length, - `Expected ${evalData.length} eval results`, - ); + const summary = r.summary as Record; + const results = r.results as Array>; - for (const row of results) { - assertDefined(row.input, "Eval result missing input"); - assertDefined(row.expected, "Eval result missing expected"); - assertDefined(row.output, "Eval result missing output"); assertEqual( - row.output as string, - row.expected as string, - "Output should equal expected", + results.length, + evalData.length, + `Expected ${evalData.length} eval results`, ); - assertHasProperty(row, "scores", "Eval result missing scores"); - const scores = row.scores as Record; - assertDefined(scores.exact_match, "Missing exact_match score"); - assertEqual(scores.exact_match as number, 1, "exact_match should be 1"); - } + for (const row of results) { + assertDefined(row.input, "Eval result missing input"); + assertDefined(row.expected, "Eval result missing expected"); + assertDefined(row.output, "Eval result missing output"); + assertEqual( + row.output as string, + row.expected as string, + "Output should equal expected", + ); + + assertHasProperty(row, "scores", "Eval result missing scores"); + const scores = row.scores as Record; + assertDefined(scores.exact_match, "Missing exact_match score"); + assertEqual(scores.exact_match as number, 1, "exact_match should be 1"); + } - assertHasProperty(summary, "scores", "Summary missing scores"); - const summaryScores = summary.scores as Record; - assertHasProperty( - summaryScores, - "exact_match", - "Summary missing exact_match scorer", - ); - const exact = summaryScores.exact_match as Record; - assertDefined(exact.score, "Summary exact_match missing score"); - assertEqual( - exact.score as number, - 1, - "Summary exact_match.score should be 1", - ); + assertHasProperty(summary, "scores", "Summary missing scores"); + const summaryScores = summary.scores as Record; + assertHasProperty( + summaryScores, + "exact_match", + "Summary missing exact_match scorer", + ); + const exact = summaryScores.exact_match as Record; + assertDefined(exact.score, "Summary exact_match missing score"); + assertEqual( + exact.score as number, + 1, + "Summary exact_match.score should be 1", + ); - return `Eval smoke test passed (${evalData.length} cases)`; -}); + return `Eval smoke test passed (${evalData.length} cases)`; + }, +); diff --git a/js/smoke/shared/src/suites/import-verification.ts b/js/smoke/shared/src/suites/import-verification.ts index 220ff39b5..2fb8a54ec 100644 --- a/js/smoke/shared/src/suites/import-verification.ts +++ b/js/smoke/shared/src/suites/import-verification.ts @@ -99,7 +99,7 @@ export interface BraintrustModule { [key: string]: unknown; } -export const testCoreLoggingExports = register( +export const testCoreLoggingExports: TestFn = register( "testCoreLoggingExports", async (module) => { assertDefined(module.initLogger, "initLogger must exist"); @@ -152,7 +152,7 @@ export const testCoreLoggingExports = register( }, ); -export const testDatasetExports = register( +export const testDatasetExports: TestFn = register( "testDatasetExports", async (module) => { assertDefined(module.initDataset, "initDataset must exist"); @@ -169,7 +169,7 @@ export const testDatasetExports = register( }, ); -export const testPromptExports = register( +export const testPromptExports: TestFn = register( "testPromptExports", async (module) => { assertDefined(module.loadPrompt, "loadPrompt must exist"); @@ -189,7 +189,7 @@ export const testPromptExports = register( }, ); -export const testExperimentExports = register( +export const testExperimentExports: TestFn = register( "testExperimentExports", async (module) => { assertDefined(module.init, "init must exist"); @@ -213,27 +213,30 @@ export const testExperimentExports = register( }, ); -export const testEvalExports = register("testEvalExports", async (module) => { - assertDefined(module.Eval, "Eval must exist"); - assertType(module.Eval, "function", "Eval must be a function"); +export const testEvalExports: TestFn = register( + "testEvalExports", + async (module) => { + assertDefined(module.Eval, "Eval must exist"); + assertType(module.Eval, "function", "Eval must be a function"); - assertDefined(module.Reporter, "Reporter must exist"); - assertType(module.Reporter, "function", "Reporter must be a function"); + assertDefined(module.Reporter, "Reporter must exist"); + assertType(module.Reporter, "function", "Reporter must be a function"); - assertDefined( - module.defaultErrorScoreHandler, - "defaultErrorScoreHandler must exist", - ); - assertType( - module.defaultErrorScoreHandler, - "function", - "defaultErrorScoreHandler must be a function", - ); + assertDefined( + module.defaultErrorScoreHandler, + "defaultErrorScoreHandler must exist", + ); + assertType( + module.defaultErrorScoreHandler, + "function", + "defaultErrorScoreHandler must be a function", + ); - return "Eval exports verified (3 runtime exports)"; -}); + return "Eval exports verified (3 runtime exports)"; + }, +); -export const testTracingExports = register( +export const testTracingExports: TestFn = register( "testTracingExports", async (module) => { assertDefined(module.traced, "traced must exist"); @@ -263,7 +266,7 @@ export const testTracingExports = register( }, ); -export const testClientWrapperExports = register( +export const testClientWrapperExports: TestFn = register( "testClientWrapperExports", async (module) => { assertDefined(module.wrapOpenAI, "wrapOpenAI must exist"); @@ -293,7 +296,7 @@ export const testClientWrapperExports = register( }, ); -export const testUtilityExports = register( +export const testUtilityExports: TestFn = register( "testUtilityExports", async (module) => { assertDefined(module.JSONAttachment, "JSONAttachment must exist"); @@ -314,7 +317,7 @@ export const testUtilityExports = register( }, ); -export const testFunctionExports = register( +export const testFunctionExports: TestFn = register( "testFunctionExports", async (module) => { assertDefined(module.invoke, "invoke must exist"); @@ -331,7 +334,7 @@ export const testFunctionExports = register( }, ); -export const testFramework2Exports = register( +export const testFramework2Exports: TestFn = register( "testFramework2Exports", async (module) => { assertDefined(module.projects, "projects must exist"); @@ -341,7 +344,7 @@ export const testFramework2Exports = register( }, ); -export const testIDGeneratorExports = register( +export const testIDGeneratorExports: TestFn = register( "testIDGeneratorExports", async (module) => { assertDefined(module.IDGenerator, "IDGenerator must exist"); @@ -356,7 +359,7 @@ export const testIDGeneratorExports = register( { requiresTestHarness: true }, ); -export const testTestingExports = register( +export const testTestingExports: TestFn = register( "testTestingExports", async (module) => { assertDefined( @@ -374,7 +377,7 @@ export const testTestingExports = register( { requiresTestHarness: true }, ); -export const testStateManagementExports = register( +export const testStateManagementExports: TestFn = register( "testStateManagementExports", async (module) => { assertDefined(module.BraintrustState, "BraintrustState must exist"); diff --git a/js/smoke/shared/src/suites/prompt-templating.ts b/js/smoke/shared/src/suites/prompt-templating.ts index dcbcc4380..d10cd870f 100644 --- a/js/smoke/shared/src/suites/prompt-templating.ts +++ b/js/smoke/shared/src/suites/prompt-templating.ts @@ -4,9 +4,9 @@ */ import { assertEqual } from "../helpers/assertions"; -import { register } from "../helpers/register"; +import { register, type TestFn } from "../helpers/register"; -export const testMustacheTemplate = register( +export const testMustacheTemplate: TestFn = register( "testMustacheTemplate", async (module) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -50,7 +50,7 @@ export const testMustacheTemplate = register( }, ); -export const testNunjucksTemplate = register( +export const testNunjucksTemplate: TestFn = register( "testNunjucksTemplate", async (module) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/js/smoke/shared/tsup.config.ts b/js/smoke/shared/tsup.config.ts deleted file mode 100644 index a7ac7c9b8..000000000 --- a/js/smoke/shared/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - format: ["cjs", "esm"], - outDir: "dist", - dts: true, - splitting: false, - clean: true, - sourcemap: true, - // No external dependencies to bundle - this is a standalone test utility package -}); diff --git a/js/smoke/shared/vite.config.ts b/js/smoke/shared/vite.config.ts new file mode 100644 index 000000000..b94d9e910 --- /dev/null +++ b/js/smoke/shared/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + pack: { + platform: "neutral", + target: "es2022", + dts: { generator: "oxc" }, + sourcemap: true, + }, +}); diff --git a/js/src/agent-assertions.test.ts b/js/src/agent-assertions.test.ts index 0e3cb5b2e..c9ee9a842 100644 --- a/js/src/agent-assertions.test.ts +++ b/js/src/agent-assertions.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, expect, expectTypeOf, test, vi } from "vitest"; +import { beforeAll, expect, expectTypeOf, test, vi } from "vite-plus/test"; import { z } from "zod"; import { agentAssertionScorer, Eval } from "./exports"; diff --git a/js/src/agent-assertions.ts b/js/src/agent-assertions.ts index 93824cb60..4f3c65b9a 100644 --- a/js/src/agent-assertions.ts +++ b/js/src/agent-assertions.ts @@ -1,6 +1,6 @@ import type { EvalScorer, EvalScorerArgs } from "./framework"; import type { SpanData } from "./trace"; -import type { BaseMetadata, DefaultMetadataType } from "./logger"; +import type { BaseMetadata, DefaultMetadataType } from "./logger/core"; import type { Score } from "../util"; type MaybePromise = T | Promise; diff --git a/js/src/attachment-capture.test.ts b/js/src/attachment-capture.test.ts index 95f09b368..d93f4873c 100644 --- a/js/src/attachment-capture.test.ts +++ b/js/src/attachment-capture.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeAll, beforeEach, expect, it, vi } from "vitest"; +import { + afterEach, + beforeAll, + beforeEach, + expect, + it, + vi, +} from "vite-plus/test"; import { _internalCaptureAttachmentsEnabled, Attachment, @@ -6,12 +13,12 @@ import { CAPTURE_ATTACHMENTS, ExternalAttachment, startSpan, - _exportsForTestingOnly, _internalGetGlobalState, initLogger, -} from "./logger"; +} from "./logger/core"; +import { _exportsForTestingOnly } from "./logger/testing"; import { configureNode } from "./node/config"; -import iso from "./isomorph"; +import iso from "#braintrust-platform"; import { processInputAttachments } from "./wrappers/attachment-utils"; import { processAttachmentsInInput } from "./instrumentation/providers/anthropic-instrumentation"; import { @@ -25,13 +32,17 @@ import { OpenAIAgentsTraceProcessor } from "./instrumentation/providers/openai-a import type { OpenAIAgentsSpan } from "./vendor-sdk-types/openai-agents"; import { extractOllamaChatInput } from "./instrumentation/providers/ollama-instrumentation"; import { openAIChannels } from "./instrumentation/providers/openai-channels"; +import { registerInstrumentation } from "./instrumentation/registrations/openai"; configureNode(); const state = _internalGetGlobalState(); let background: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; -beforeAll(() => _exportsForTestingOnly.simulateLoginForTests()); +beforeAll(() => { + registerInstrumentation(); + return _exportsForTestingOnly.simulateLoginForTests(); +}); beforeEach(() => { vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", undefined); state.captureAttachments = undefined; diff --git a/js/src/auto-instrumentations/bundler/next.ts b/js/src/auto-instrumentations/bundler/next.ts index 5124b5568..639672703 100644 --- a/js/src/auto-instrumentations/bundler/next.ts +++ b/js/src/auto-instrumentations/bundler/next.ts @@ -245,10 +245,7 @@ function addBraintrustTurbopackRule( function getWebpackLoaderPath(): string { const packageJsonPath = requireFromProject.resolve("braintrust/package.json"); - return join( - dirname(packageJsonPath), - "dist/auto-instrumentations/bundler/webpack-loader.cjs", - ); + return join(dirname(packageJsonPath), "dist/loaders/webpack-loader.cjs"); } function getNextMajorVersion(): number | undefined { diff --git a/js/src/auto-instrumentations/bundler/plugin.test.ts b/js/src/auto-instrumentations/bundler/plugin.test.ts index 695a243cd..7ba836162 100644 --- a/js/src/auto-instrumentations/bundler/plugin.test.ts +++ b/js/src/auto-instrumentations/bundler/plugin.test.ts @@ -1,5 +1,5 @@ import { createRequire } from "node:module"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { unplugin } from "./plugin"; const require = createRequire(import.meta.url); diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index 98361aaea..561144130 100644 --- a/js/src/auto-instrumentations/bundler/plugin.ts +++ b/js/src/auto-instrumentations/bundler/plugin.ts @@ -1,4 +1,4 @@ -import { createUnplugin } from "unplugin"; +import { createUnplugin, type UnpluginInstance } from "unplugin"; import { create, type InstrumentationConfig } from "../orchestrion-js"; import { extname, isAbsolute, join, sep } from "path"; import { readFileSync } from "fs"; @@ -43,127 +43,128 @@ function getModuleVersion(basedir: string): string | undefined { return undefined; // No version found } -export const unplugin = createUnplugin((options = {}) => { - const browser = options.browser ?? false; - const allInstrumentations = getDefaultAutoInstrumentationConfigs( - options.instrumentations, - ); - - // Create the code transformer instrumentor - const instrumentationMatcher = create(allInstrumentations); - - return { - name: "code-transformer", - enforce: "pre", - transformInclude(id: string) { - const pathWithoutQuery = id.split("?", 1)[0]; - if (!pathWithoutQuery) { - return false; - } - if (pathWithoutQuery.startsWith("file:")) { - try { - return isAbsolute(fileURLToPath(pathWithoutQuery)); - } catch { +export const unplugin: UnpluginInstance = + createUnplugin((options = {}) => { + const browser = options.browser ?? false; + const allInstrumentations = getDefaultAutoInstrumentationConfigs( + options.instrumentations, + ); + + // Create the code transformer instrumentor + const instrumentationMatcher = create(allInstrumentations); + + return { + name: "code-transformer", + enforce: "pre", + transformInclude(id: string) { + const pathWithoutQuery = id.split("?", 1)[0]; + if (!pathWithoutQuery) { return false; } - } - return isAbsolute(pathWithoutQuery); - }, - transform(code: string, id: string) { - if (!id) { - // Some modules apparently don't have an id? - return null; - } - - // Vite and Rollup may append cache-busting queries or fragments to - // real filesystem module IDs. They are not part of the package path - // and prevent exact instrumentation file paths from matching. - const cleanId = id.replace(/[?#].*$/, ""); - - // Convert file:// URLs to regular paths at entry point - // Node.js ESM loader hooks provide file:// URLs, but downstream code expects paths - const filePath = cleanId.startsWith("file:") - ? fileURLToPath(cleanId) - : cleanId; - - // Determine if this is an ES module using multiple methods for accurate detection - const ext = extname(filePath); - let isModule = ext === ".mjs" || ext === ".ts" || ext === ".tsx"; - - // For .js files, use content analysis for module detection - if (ext === ".js") { - isModule = code.includes("export ") || code.includes("import "); - } - - // Try to get module details from the file path - // IMPORTANT: module-details-from-path uses path.sep to split paths. - // On Windows (path.sep = '\'), we need to convert forward slashes to backslashes. - // On Unix (path.sep = '/'), paths should already use forward slashes. - // Some bundlers (like Vite/Rollup) may pass paths with forward slashes even on Windows. - const normalizedForPlatform = filePath.split("/").join(sep); - const moduleDetails = moduleDetailsFromPath(normalizedForPlatform); - - // If no module details found, the file is not part of a module - if (!moduleDetails) { - return null; - } - - // npm aliases retain the canonical package name in package.json even - // though their node_modules directory uses the alias. Match configs - // against that canonical name so aliased dependencies are instrumented. - const moduleName = - getPackageName(moduleDetails.basedir) ?? moduleDetails.name; - // Normalize the module path for Windows compatibility (WASM transformer expects forward slashes) - const normalizedModulePath = moduleDetails.path.replace(/\\/g, "/"); - const moduleVersion = getModuleVersion(moduleDetails.basedir); - - // Per-package source patches (see loader/special-case-patches.ts). - // Same anti-pattern fallback the runtime loader uses — mirrored here - // so bundled apps get the patches without relying on hook.mjs. - const patched = applySpecialCasePatch({ - packageName: moduleName, - modulePath: normalizedModulePath, - source: code, - format: isModule ? "esm" : "cjs", - browser, - }); - if (patched !== null) { - return { code: patched, map: null }; - } - - // If no version found - if (!moduleVersion) { - console.warn( - `No 'package.json' version found for module ${moduleName} at ${moduleDetails.basedir}. Skipping transformation.`, + if (pathWithoutQuery.startsWith("file:")) { + try { + return isAbsolute(fileURLToPath(pathWithoutQuery)); + } catch { + return false; + } + } + return isAbsolute(pathWithoutQuery); + }, + transform(code: string, id: string) { + if (!id) { + // Some modules apparently don't have an id? + return null; + } + + // Vite and Rollup may append cache-busting queries or fragments to + // real filesystem module IDs. They are not part of the package path + // and prevent exact instrumentation file paths from matching. + const cleanId = id.replace(/[?#].*$/, ""); + + // Convert file:// URLs to regular paths at entry point + // Node.js ESM loader hooks provide file:// URLs, but downstream code expects paths + const filePath = cleanId.startsWith("file:") + ? fileURLToPath(cleanId) + : cleanId; + + // Determine if this is an ES module using multiple methods for accurate detection + const ext = extname(filePath); + let isModule = ext === ".mjs" || ext === ".ts" || ext === ".tsx"; + + // For .js files, use content analysis for module detection + if (ext === ".js") { + isModule = code.includes("export ") || code.includes("import "); + } + + // Try to get module details from the file path + // IMPORTANT: module-details-from-path uses path.sep to split paths. + // On Windows (path.sep = '\'), we need to convert forward slashes to backslashes. + // On Unix (path.sep = '/'), paths should already use forward slashes. + // Some bundlers (like Vite/Rollup) may pass paths with forward slashes even on Windows. + const normalizedForPlatform = filePath.split("/").join(sep); + const moduleDetails = moduleDetailsFromPath(normalizedForPlatform); + + // If no module details found, the file is not part of a module + if (!moduleDetails) { + return null; + } + + // npm aliases retain the canonical package name in package.json even + // though their node_modules directory uses the alias. Match configs + // against that canonical name so aliased dependencies are instrumented. + const moduleName = + getPackageName(moduleDetails.basedir) ?? moduleDetails.name; + // Normalize the module path for Windows compatibility (WASM transformer expects forward slashes) + const normalizedModulePath = moduleDetails.path.replace(/\\/g, "/"); + const moduleVersion = getModuleVersion(moduleDetails.basedir); + + // Per-package source patches (see loader/special-case-patches.ts). + // Same anti-pattern fallback the runtime loader uses — mirrored here + // so bundled apps get the patches without relying on hook.mjs. + const patched = applySpecialCasePatch({ + packageName: moduleName, + modulePath: normalizedModulePath, + source: code, + format: isModule ? "esm" : "cjs", + browser, + }); + if (patched !== null) { + return { code: patched, map: null }; + } + + // If no version found + if (!moduleVersion) { + console.warn( + `No 'package.json' version found for module ${moduleName} at ${moduleDetails.basedir}. Skipping transformation.`, + ); + return null; + } + + // Try to get a transformer for this file + const transformer = instrumentationMatcher.getTransformer( + moduleName, + moduleVersion, + normalizedModulePath, ); - return null; - } - - // Try to get a transformer for this file - const transformer = instrumentationMatcher.getTransformer( - moduleName, - moduleVersion, - normalizedModulePath, - ); - - if (!transformer) { - // No instrumentations match this file - return null; - } - - try { - // Transform the code - const moduleType = isModule ? "esm" : "cjs"; - const result = transformer.transform(code, moduleType); - return { - code: result.code, - map: result.map, - }; - } catch (error) { - // If transformation fails, warn and return original code - console.warn(`Code transformation failed for ${id}: ${error}`); - return null; - } - }, - }; -}); + + if (!transformer) { + // No instrumentations match this file + return null; + } + + try { + // Transform the code + const moduleType = isModule ? "esm" : "cjs"; + const result = transformer.transform(code, moduleType); + return { + code: result.code, + map: result.map, + }; + } catch (error) { + // If transformation fails, warn and return original code + console.warn(`Code transformation failed for ${id}: ${error}`); + return null; + } + }, + }; + }); diff --git a/js/src/auto-instrumentations/configs/anthropic.ts b/js/src/auto-instrumentations/configs/anthropic.ts index 6f13c05c0..1937023b8 100644 --- a/js/src/auto-instrumentations/configs/anthropic.ts +++ b/js/src/auto-instrumentations/configs/anthropic.ts @@ -133,35 +133,30 @@ export const anthropicConfigs: InstrumentationConfig[] = [ // Managed Agents Sessions event streams are discovered automatically, but // remain passive until the user calls collectAnthropicSession() on the // returned stream. - ...["mjs", "js"].map( - (extension): InstrumentationConfig => ({ - channelName: anthropicChannels.betaSessionsEventsStream.channelName, - module: { - name: "@anthropic-ai/sdk", - versionRange: ">=0.86.0", - filePath: `resources/beta/sessions/events.${extension}`, - }, - functionQuery: { - className: "Events", - methodName: "stream", - kind: "Async", - }, - }), - ), - ...["mjs", "js"].map( - (extension): InstrumentationConfig => ({ - channelName: - anthropicChannels.betaSessionsThreadsEventsStream.channelName, - module: { - name: "@anthropic-ai/sdk", - versionRange: ">=0.86.0", - filePath: `resources/beta/sessions/threads/events.${extension}`, - }, - functionQuery: { - className: "Events", - methodName: "stream", - kind: "Async", - }, - }), - ), + ...["mjs", "js"].map((extension): InstrumentationConfig => ({ + channelName: anthropicChannels.betaSessionsEventsStream.channelName, + module: { + name: "@anthropic-ai/sdk", + versionRange: ">=0.86.0", + filePath: `resources/beta/sessions/events.${extension}`, + }, + functionQuery: { + className: "Events", + methodName: "stream", + kind: "Async", + }, + })), + ...["mjs", "js"].map((extension): InstrumentationConfig => ({ + channelName: anthropicChannels.betaSessionsThreadsEventsStream.channelName, + module: { + name: "@anthropic-ai/sdk", + versionRange: ">=0.86.0", + filePath: `resources/beta/sessions/threads/events.${extension}`, + }, + functionQuery: { + className: "Events", + methodName: "stream", + kind: "Async", + }, + })), ]; diff --git a/js/src/auto-instrumentations/configs/bedrock-runtime.test.ts b/js/src/auto-instrumentations/configs/bedrock-runtime.test.ts index 77b3997b1..9a19503f9 100644 --- a/js/src/auto-instrumentations/configs/bedrock-runtime.test.ts +++ b/js/src/auto-instrumentations/configs/bedrock-runtime.test.ts @@ -1,5 +1,5 @@ import { create, type ModuleType } from "@apm-js-collab/code-transformer"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { bedrockRuntimeConfigs } from "./bedrock-runtime"; const CJS_CLIENT_SOURCE = ` diff --git a/js/src/auto-instrumentations/configs/cloudflare-think.test.ts b/js/src/auto-instrumentations/configs/cloudflare-think.test.ts index 0deb66b31..a39f234dd 100644 --- a/js/src/auto-instrumentations/configs/cloudflare-think.test.ts +++ b/js/src/auto-instrumentations/configs/cloudflare-think.test.ts @@ -1,5 +1,5 @@ import { create } from "@apm-js-collab/code-transformer"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { cloudflareThinkConfigs } from "./cloudflare-think"; describe("Cloudflare Think transformation", () => { diff --git a/js/src/auto-instrumentations/configs/pi-coding-agent.test.ts b/js/src/auto-instrumentations/configs/pi-coding-agent.test.ts index 4446b7872..fa554eb79 100644 --- a/js/src/auto-instrumentations/configs/pi-coding-agent.test.ts +++ b/js/src/auto-instrumentations/configs/pi-coding-agent.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { readDisabledInstrumentationEnvConfig } from "../../instrumentation/config"; import { getDefaultInstrumentationConfigs } from "./all"; import { piCodingAgentConfigs } from "./pi-coding-agent"; diff --git a/js/src/auto-instrumentations/configs/strands-agent-sdk.test.ts b/js/src/auto-instrumentations/configs/strands-agent-sdk.test.ts index 58dcff867..99c53778b 100644 --- a/js/src/auto-instrumentations/configs/strands-agent-sdk.test.ts +++ b/js/src/auto-instrumentations/configs/strands-agent-sdk.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { readDisabledInstrumentationEnvConfig } from "../../instrumentation/config"; import { getDefaultInstrumentationConfigs } from "./all"; import { strandsAgentSDKConfigs } from "./strands-agent-sdk"; diff --git a/js/src/auto-instrumentations/hook.mts b/js/src/auto-instrumentations/hook.mts index 6ce38179d..7dcd79056 100644 --- a/js/src/auto-instrumentations/hook.mts +++ b/js/src/auto-instrumentations/hook.mts @@ -1,3 +1,4 @@ +import "../auto.js"; /** * Unified loader hook for auto-instrumentation (ESM + CJS). * @@ -14,12 +15,6 @@ */ import { register } from "node:module"; -import { - isInstrumentationIntegrationDisabled, - readDisabledInstrumentationEnvConfig, -} from "../instrumentation/config.js"; -import { BraintrustObservabilityExporter } from "../wrappers/mastra.js"; -import { installMastraExporterFactory } from "./loader/mastra-observability-patch.js"; import { getDefaultAutoInstrumentationConfigs } from "./configs/all.js"; import { ModulePatch } from "./loader/cjs-patch.js"; import { GLOBAL_INSTRUMENTATION_HOOKS_PROTOCOL_VERSION } from "../global-instrumentation-hooks.js"; @@ -34,17 +29,6 @@ const alreadyApplied = state.applied; if (!alreadyApplied) { const allConfigs = getDefaultAutoInstrumentationConfigs(); - // Expose the Mastra exporter factory on globalThis so the loader patches - // for `@mastra/core` / `@mastra/observability` can find it without having - // to resolve `braintrust` from the user's module graph. Skipped when the - // user opts out via `BRAINTRUST_DISABLE_INSTRUMENTATION=mastra`. - const disabled = readDisabledInstrumentationEnvConfig( - process.env.BRAINTRUST_DISABLE_INSTRUMENTATION, - ).integrations; - if (!isInstrumentationIntegrationDisabled(disabled, "mastra")) { - installMastraExporterFactory(() => new BraintrustObservabilityExporter()); - } - // 1. Register ESM loader for ESM modules register("./loader/esm-hook.mjs", { parentURL: import.meta.url, diff --git a/js/src/auto-instrumentations/import-in-the-middle/package.json b/js/src/auto-instrumentations/import-in-the-middle/package.json new file mode 100644 index 000000000..0fef86e33 --- /dev/null +++ b/js/src/auto-instrumentations/import-in-the-middle/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "type": "commonjs" +} diff --git a/js/src/auto-instrumentations/loader/cjs-patch.ts b/js/src/auto-instrumentations/loader/cjs-patch.ts index d94511563..1046c57b4 100644 --- a/js/src/auto-instrumentations/loader/cjs-patch.ts +++ b/js/src/auto-instrumentations/loader/cjs-patch.ts @@ -32,7 +32,7 @@ export class ModulePatch { * If a module is found that has an instrumentator, it will transform the code before compiling it * with global hook calls. */ - patch() { + patch(): void { const self = this; this.modulePrototype._compile = function wrappedCompile(...args: any[]) { const [content, filename] = args; @@ -90,7 +90,7 @@ export class ModulePatch { * Restores the original Module.prototype._compile method * **Note**: This is intended to be used in testing only. */ - unpatch() { + unpatch(): void { this.modulePrototype._compile = this.originalCompile; } } diff --git a/js/src/auto-instrumentations/loader/mastra-observability-patch.test.ts b/js/src/auto-instrumentations/loader/mastra-observability-patch.test.ts index 60ee5aac7..389ae0568 100644 --- a/js/src/auto-instrumentations/loader/mastra-observability-patch.test.ts +++ b/js/src/auto-instrumentations/loader/mastra-observability-patch.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { classifyMastraTarget, patchMastraSource, diff --git a/js/src/auto-instrumentations/orchestrion-js/transforms.test.ts b/js/src/auto-instrumentations/orchestrion-js/transforms.test.ts index a37a0ac82..eb47edae9 100644 --- a/js/src/auto-instrumentations/orchestrion-js/transforms.test.ts +++ b/js/src/auto-instrumentations/orchestrion-js/transforms.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { Transformer } from "./transformer"; describe("orchestrion transforms", () => { diff --git a/js/src/auto-instrumentations/require-in-the-middle/package.json b/js/src/auto-instrumentations/require-in-the-middle/package.json new file mode 100644 index 000000000..0fef86e33 --- /dev/null +++ b/js/src/auto-instrumentations/require-in-the-middle/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "type": "commonjs" +} diff --git a/js/src/auto.ts b/js/src/auto.ts new file mode 100644 index 000000000..950dc59e2 --- /dev/null +++ b/js/src/auto.ts @@ -0,0 +1,6 @@ +import { registerInstrumentationConsumers } from "./instrumentation/instrumentation-consumers"; +import { wrapOpenAI } from "./wrappers/oai"; + +registerInstrumentationConsumers(); +// Compatibility with OpenAI clients created by the remote function runtime. +globalThis.__inherited_braintrust_wrap_openai = wrapOpenAI; diff --git a/js/src/background-logger.test.ts b/js/src/background-logger.test.ts index 9fee6b26f..de01edca9 100644 --- a/js/src/background-logger.test.ts +++ b/js/src/background-logger.test.ts @@ -1,5 +1,5 @@ -import { afterEach, describe, expect, test, vi } from "vitest"; -import { BraintrustState } from "./logger"; +import { afterEach, describe, expect, test, vi } from "vite-plus/test"; +import { BraintrustState } from "./logger/core"; import { configureNode } from "./node/config"; import { Queue } from "./queue"; import { LazyValue } from "./util"; diff --git a/js/src/browser/config.ts b/js/src/browser/config.ts index c2d649bdb..34e579d3f 100644 --- a/js/src/browser/config.ts +++ b/js/src/browser/config.ts @@ -1,63 +1,7 @@ -import iso from "../isomorph"; -import { _internalSetInitialState } from "../logger"; -import { registry } from "../instrumentation/registry"; +import iso from "#braintrust-platform"; +import { createBrowserIsomorph } from "./platform"; -// This is copied from next.js. It seems they define AsyncLocalStorage in the edge -// environment, even though it's not defined in the browser. -import type { AsyncLocalStorage as NodeAsyncLocalStorage } from "node:async_hooks"; - -declare global { - var AsyncLocalStorage: typeof NodeAsyncLocalStorage; - var __BRAINTRUST_BROWSER_PKG__: boolean | undefined; -} - -let browserConfigured = false; - -/** - * Configure the isomorph for browser environments. - */ +/** @internal Configure source-level tests and compatibility integrations. */ export function configureBrowser(): void { - if (browserConfigured) { - return; - } - - iso.buildType = "browser"; - - // Try to use global AsyncLocalStorage (edge runtime like Next.js) - try { - if (typeof AsyncLocalStorage !== "undefined") { - iso.newAsyncLocalStorage = () => new AsyncLocalStorage(); - } - } catch { - // Ignore - } - - iso.getEnv = (name: string) => { - if (typeof process === "undefined" || typeof process.env === "undefined") { - return undefined; - } - return process.env[name]; - }; - iso.getBraintrustApiKey = async () => { - const value = iso.getEnv("BRAINTRUST_API_KEY"); - return value?.trim() ? value : undefined; - }; - - iso.hash = (data: string): string => { - let hash = 0; - for (let i = 0; i < data.length; i++) { - const char = data.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; - } - const hashHex = (hash >>> 0).toString(16).padStart(8, "0"); - return hashHex.repeat(8).substring(0, 64); - }; - - _internalSetInitialState(); - - // Enable auto-instrumentation - registry.enable(); - - browserConfigured = true; + Object.assign(iso, createBrowserIsomorph()); } diff --git a/js/src/browser/index.ts b/js/src/browser/index.ts index dfbca964f..376bc1cf9 100644 --- a/js/src/browser/index.ts +++ b/js/src/browser/index.ts @@ -9,8 +9,4 @@ * import * as braintrust from '@braintrust/browser'; */ -import { configureBrowser } from "./config"; - -configureBrowser(); - export * from "../exports"; diff --git a/js/src/browser/isomorph.ts b/js/src/browser/isomorph.ts new file mode 100644 index 000000000..cc3d9bb3c --- /dev/null +++ b/js/src/browser/isomorph.ts @@ -0,0 +1,10 @@ +import type { Common } from "../isomorph-base"; +export type { + CallerLocation, + IsoAsyncLocalStorage, + IsoTracingChannel, + IsoChannelHandlers, +} from "../isomorph-base"; +import { createBrowserIsomorph } from "./platform"; +const iso: Common = /* @__PURE__ */ createBrowserIsomorph(); +export default iso; diff --git a/js/src/browser/platform.ts b/js/src/browser/platform.ts new file mode 100644 index 000000000..aeedacdb8 --- /dev/null +++ b/js/src/browser/platform.ts @@ -0,0 +1,49 @@ +import { createIsomorph, type Common } from "../isomorph-base"; + +// This is copied from next.js. It seems they define AsyncLocalStorage in the edge +// environment, even though it's not defined in the browser. +import type { AsyncLocalStorage as NodeAsyncLocalStorage } from "node:async_hooks"; + +declare global { + var AsyncLocalStorage: typeof NodeAsyncLocalStorage; + var __BRAINTRUST_BROWSER_PKG__: boolean | undefined; +} + +/** + * Configure the isomorph for browser environments. + */ +export function createBrowserIsomorph(): Common { + const iso = createIsomorph(); + + iso.buildType = "browser"; + + const fallbackAsyncLocalStorage = iso.newAsyncLocalStorage; + iso.newAsyncLocalStorage = () => + typeof AsyncLocalStorage !== "undefined" + ? new AsyncLocalStorage() + : fallbackAsyncLocalStorage(); + + iso.getEnv = (name: string) => { + if (typeof process === "undefined" || typeof process.env === "undefined") { + return undefined; + } + return process.env[name]; + }; + iso.getBraintrustApiKey = async () => { + const value = iso.getEnv("BRAINTRUST_API_KEY"); + return value?.trim() ? value : undefined; + }; + + iso.hash = (data: string): string => { + let hash = 0; + for (let i = 0; i < data.length; i++) { + const char = data.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + const hashHex = (hash >>> 0).toString(16).padStart(8, "0"); + return hashHex.repeat(8).substring(0, 64); + }; + + return iso; +} diff --git a/js/src/debug-logger.test.ts b/js/src/debug-logger.test.ts index 73e34ff87..df50f1597 100644 --- a/js/src/debug-logger.test.ts +++ b/js/src/debug-logger.test.ts @@ -1,11 +1,15 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; - +import { installGlobalHookErrorReporter } from "./debug-logger"; import { - BraintrustState, - _exportsForTestingOnly, - initLogger, - login, -} from "./logger"; + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from "vite-plus/test"; + +import { BraintrustState, initLogger, login } from "./logger/core"; +import { _exportsForTestingOnly } from "./logger/testing"; import { debugLogger, getEnvDebugLogLevel, @@ -75,6 +79,7 @@ describe("debug logger", () => { test("global hook failures are logged without escaping the provider call", () => { process.env.BRAINTRUST_DEBUG_LOG_LEVEL = "error"; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + installGlobalHookErrorReporter(); const subscriberError = new Error("subscriber failed"); const channel = newGlobalTracingChannel>( `test:debug-logger:${Math.random()}`, diff --git a/js/src/debug-logger.ts b/js/src/debug-logger.ts index 24861c7f4..23a18dfee 100644 --- a/js/src/debug-logger.ts +++ b/js/src/debug-logger.ts @@ -1,4 +1,4 @@ -import iso from "./isomorph"; +import iso from "#braintrust-platform"; import { setGlobalHookErrorReporter } from "./global-instrumentation-hooks"; export type DebugLogLevel = "error" | "warn" | "info" | "debug"; @@ -169,13 +169,17 @@ function createDebugLogger(state?: DebugLoggerStateLike): DebugLoggerMethods { }; } -export const debugLogger = { +export const debugLogger: DebugLoggerMethods & { + forState(state: DebugLoggerStateLike | undefined): DebugLoggerMethods; +} = { ...createDebugLogger(), forState(state: DebugLoggerStateLike | undefined): DebugLoggerMethods { return createDebugLogger(state); }, }; -setGlobalHookErrorReporter((error) => { - debugLogger.error("Global instrumentation hook error:", error); -}); +export function installGlobalHookErrorReporter(): void { + setGlobalHookErrorReporter((error) => { + debugLogger.error("Global instrumentation hook error:", error); + }); +} diff --git a/js/src/edge-light/config.ts b/js/src/edge-light/config.ts index 20302d2cd..d67f54433 100644 --- a/js/src/edge-light/config.ts +++ b/js/src/edge-light/config.ts @@ -1,49 +1,7 @@ -import iso from "../isomorph"; -import { _internalSetInitialState } from "../logger"; -import { resolveRuntimeAsyncLocalStorage } from "../runtime-async-local-storage"; -import { registry } from "../instrumentation/registry"; +import iso from "#braintrust-platform"; +import { createEdgeLightIsomorph } from "./platform"; -let edgeLightConfigured = false; - -/** - * Configure the isomorph for edge-light runtime environments (Vercel Edge, Next.js Edge, etc.). - */ +/** @internal Configure source-level tests and compatibility integrations. */ export function configureEdgeLight(): void { - if (edgeLightConfigured) { - return; - } - - iso.buildType = "edge-light"; - - const runtimeAsyncLocalStorage = resolveRuntimeAsyncLocalStorage(); - - if (runtimeAsyncLocalStorage) { - iso.newAsyncLocalStorage = () => new runtimeAsyncLocalStorage(); - } - - iso.getEnv = (name: string) => { - if (typeof process === "undefined" || typeof process.env === "undefined") { - return undefined; - } - return process.env[name]; - }; - iso.getBraintrustApiKey = async () => { - const value = iso.getEnv("BRAINTRUST_API_KEY"); - return value?.trim() ? value : undefined; - }; - - iso.hash = (data: string): string => { - let hash = 0; - for (let i = 0; i < data.length; i++) { - const char = data.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; - } - const hashHex = (hash >>> 0).toString(16).padStart(8, "0"); - return hashHex.repeat(8).substring(0, 64); - }; - - _internalSetInitialState(); - registry.enable(); - edgeLightConfigured = true; + Object.assign(iso, createEdgeLightIsomorph()); } diff --git a/js/src/edge-light/index.ts b/js/src/edge-light/index.ts index 800419f2a..a6697332a 100644 --- a/js/src/edge-light/index.ts +++ b/js/src/edge-light/index.ts @@ -5,8 +5,4 @@ * Next.js Edge Runtime, and other edge platforms that support AsyncLocalStorage. */ -import { configureEdgeLight } from "./config"; - -configureEdgeLight(); - export * from "../exports"; diff --git a/js/src/edge-light/isomorph.ts b/js/src/edge-light/isomorph.ts new file mode 100644 index 000000000..51d6bc4a5 --- /dev/null +++ b/js/src/edge-light/isomorph.ts @@ -0,0 +1,10 @@ +import type { Common } from "../isomorph-base"; +export type { + CallerLocation, + IsoAsyncLocalStorage, + IsoTracingChannel, + IsoChannelHandlers, +} from "../isomorph-base"; +import { createEdgeLightIsomorph } from "./platform"; +const iso: Common = /* @__PURE__ */ createEdgeLightIsomorph(); +export default iso; diff --git a/js/src/edge-light/platform.ts b/js/src/edge-light/platform.ts new file mode 100644 index 000000000..5fb9a5827 --- /dev/null +++ b/js/src/edge-light/platform.ts @@ -0,0 +1,43 @@ +import { createIsomorph, type Common } from "../isomorph-base"; +import { resolveRuntimeAsyncLocalStorage } from "../runtime-async-local-storage"; + +/** + * Configure the isomorph for edge-light runtime environments (Vercel Edge, Next.js Edge, etc.). + */ +export function createEdgeLightIsomorph(): Common { + const iso = createIsomorph(); + + iso.buildType = "edge-light"; + + const fallbackAsyncLocalStorage = iso.newAsyncLocalStorage; + iso.newAsyncLocalStorage = () => { + const RuntimeAsyncLocalStorage = resolveRuntimeAsyncLocalStorage(); + return RuntimeAsyncLocalStorage + ? new RuntimeAsyncLocalStorage() + : fallbackAsyncLocalStorage(); + }; + + iso.getEnv = (name: string) => { + if (typeof process === "undefined" || typeof process.env === "undefined") { + return undefined; + } + return process.env[name]; + }; + iso.getBraintrustApiKey = async () => { + const value = iso.getEnv("BRAINTRUST_API_KEY"); + return value?.trim() ? value : undefined; + }; + + iso.hash = (data: string): string => { + let hash = 0; + for (let i = 0; i < data.length; i++) { + const char = data.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + const hashHex = (hash >>> 0).toString(16).padStart(8, "0"); + return hashHex.repeat(8).substring(0, 64); + }; + + return iso; +} diff --git a/js/src/edge-runtime-bootstrap.test.ts b/js/src/edge-runtime-bootstrap.test.ts index 7cfce0fa8..c21387080 100644 --- a/js/src/edge-runtime-bootstrap.test.ts +++ b/js/src/edge-runtime-bootstrap.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from "vite-plus/test"; type EdgeEntrypoint = "./edge-light/index" | "./workerd/index"; const braintrustStateSymbol = Symbol.for("braintrust-state"); @@ -23,6 +30,11 @@ describe.each([ vi.restoreAllMocks(); Reflect.deleteProperty(globalThis, braintrustStateSymbol); + Reflect.deleteProperty( + globalThis, + Symbol.for("braintrust.registry.global-hooks.v1"), + ); + Reflect.deleteProperty(globalThis, "__braintrust_instrumentation_hooks"); if (originalAsyncLocalStorageDescriptor) { Object.defineProperty( @@ -66,6 +78,8 @@ describe.each([ get: () => getBuiltinModule, }); + const platform = await import(entrypoint.replace("/index", "/config")); + (platform.configureWorkerd ?? platform.configureEdgeLight)(); const braintrust = await import(entrypoint); const { _exportsForTestingOnly } = await import("./logger"); @@ -112,6 +126,8 @@ describe.each([ }); test("wrapAISDK logs spans in edge runtimes", async () => { + const platform = await import(entrypoint.replace("/index", "/config")); + (platform.configureWorkerd ?? platform.configureEdgeLight)(); const braintrust = await import(entrypoint); const { _exportsForTestingOnly } = await import("./logger"); diff --git a/js/src/eval-parameters.ts b/js/src/eval-parameters.ts index 60d75560d..954216862 100644 --- a/js/src/eval-parameters.ts +++ b/js/src/eval-parameters.ts @@ -1,5 +1,5 @@ import Ajv from "ajv"; -import { Prompt, RemoteEvalParameters } from "./logger"; +import { Prompt, RemoteEvalParameters } from "./logger/prompts"; import { promptDefinitionToPromptData, type PromptDefinitionWithTools, diff --git a/js/src/exports.ts b/js/src/exports.ts index 2312691ed..45050071a 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -11,20 +11,16 @@ export type { PropagationContext, Span, StartSpanArgs, -} from "./logger"; +} from "./logger/core"; export { Attachment, BaseAttachment, BraintrustState, - Dataset, - Experiment, ExternalAttachment, JSONAttachment, Logger, NOOP_SPAN, - Prompt, ReadonlyAttachment, - ReadonlyExperiment, _internalCaptureAttachmentsEnabled, currentExperiment, currentLogger, @@ -33,13 +29,8 @@ export { ContextManager, extractTraceContextFromHeaders, flush, - getPromptVersions, - init, injectTraceContext, - initDataset, initLogger, - loadParameters, - loadPrompt, logError, login, loginToState, @@ -51,7 +42,15 @@ export { withCurrent, withParent, wrapTraced, -} from "./logger"; +} from "./logger/core"; +export { Dataset, initDataset } from "./logger/datasets"; +export { Experiment, ReadonlyExperiment, init } from "./logger/experiments"; +export { + Prompt, + getPromptVersions, + loadParameters, + loadPrompt, +} from "./logger/prompts"; export { registerSandbox } from "./sandbox"; diff --git a/js/src/framework.test.ts b/js/src/framework.test.ts index 7e1d1e410..9666dd090 100644 --- a/js/src/framework.test.ts +++ b/js/src/framework.test.ts @@ -6,7 +6,7 @@ import { beforeEach, afterEach, vi, -} from "vitest"; +} from "vite-plus/test"; import { type ExperimentEvent } from "../util"; import { defaultErrorScoreHandler, @@ -16,14 +16,14 @@ import { _internalPrepareEvaluatorScore, type OneOrMoreScores, } from "./framework"; +import { _exportsForTestingOnly } from "./logger/testing"; import { - _exportsForTestingOnly, BraintrustState, initLogger, injectTraceContext, TestBackgroundLogger, _internalWithParent, -} from "./logger"; +} from "./logger/core"; import { parseBaggage } from "./propagation"; import { configureNode } from "./node/config"; import type { ProgressReporter } from "./reporters/types"; diff --git a/js/src/framework.ts b/js/src/framework.ts index 9a3d09710..a9cf277b8 100644 --- a/js/src/framework.ts +++ b/js/src/framework.ts @@ -18,7 +18,7 @@ import type { import { queue } from "async"; import { v5 as uuidv5 } from "uuid"; -import iso from "./isomorph"; +import iso from "#braintrust-platform"; import { debugLogger } from "./debug-logger"; import { GenericFunction } from "./framework-types"; import { CodeFunction, CodePrompt, CodeParameters } from "./framework2"; @@ -26,18 +26,11 @@ import { Trace, LocalTrace } from "./trace"; import { BaseMetadata, BraintrustState, - Dataset, DefaultMetadataType, EvalCase, - Experiment, - ExperimentSummary, - FullInitOptions, NOOP_SPAN, - type ParametersRef, - RemoteEvalParameters, Span, StartSpanArgs, - init as _initExperiment, currentSpan, flush, logError as logSpanError, @@ -46,7 +39,16 @@ import { withCurrent, _internalWithParent, _internalGetGlobalState, -} from "./logger"; +} from "./logger/core"; +import { Dataset } from "./logger/datasets"; +import { + Experiment, + ExperimentSummary, + FullInitOptions, + type ParametersRef, + init as _initExperiment, +} from "./logger/experiments"; +import { RemoteEvalParameters } from "./logger/prompts"; import type { ProgressReporter } from "./reporters/types"; import { SimpleProgressReporter } from "./reporters/progress"; import type { @@ -631,13 +633,15 @@ declare global { var _lazy_load: boolean; } -globalThis._evals = { - functions: [], - prompts: [], - parameters: [], - evaluators: {}, - reporters: {}, -}; +export function getEvaluatorRegistry(): EvaluatorFile { + return (globalThis._evals ??= { + functions: [], + prompts: [], + parameters: [], + evaluators: {}, + reporters: {}, + }); +} interface EvalOptions { /** @@ -717,7 +721,7 @@ interface EvalOptions { enableCache?: boolean; } -export function _initializeSpanContext() { +export function _initializeSpanContext(): void { // This only needs to be set once, but Eval(), Task(), etc. are the only time // we get to run code while importing a module, so use it to // grab these values. @@ -748,11 +752,11 @@ export async function Eval< : reporterOrOpts; let evalName = makeEvalName(name, evaluator.experimentName); - if (globalThis._evals.evaluators[evalName]) { - evalName = `${evalName}_${Object.keys(_evals).length}`; + if (getEvaluatorRegistry().evaluators[evalName]) { + evalName = `${evalName}_${Object.keys(getEvaluatorRegistry().evaluators).length}`; } if (globalThis._lazy_load) { - globalThis._evals.evaluators[evalName] = { + getEvaluatorRegistry().evaluators[evalName] = { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions evaluator: { evalName, @@ -876,12 +880,12 @@ export function Reporter( reporter: ReporterBody, ): ReporterDef { const ret = { name, ...reporter }; - if (_evals.reporters[name]) { + if (getEvaluatorRegistry().reporters[name]) { throw new Error(`Reporter ${name} already exists`); } if (globalThis._lazy_load) { - _evals.reporters[name] = ret; + getEvaluatorRegistry().reporters[name] = ret; } return ret; @@ -898,7 +902,7 @@ function scorerName( export function classifierName( classifier: EvalClassifier, classifier_idx: number, -) { +): string { return classifier.name || `classifier_${classifier_idx}`; } diff --git a/js/src/framework2.test.ts b/js/src/framework2.test.ts index 1097f2a26..bf31b2ea2 100644 --- a/js/src/framework2.test.ts +++ b/js/src/framework2.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeAll, expect, expectTypeOf, test, vi } from "vitest"; +import { + afterEach, + beforeAll, + expect, + expectTypeOf, + test, + vi, +} from "vite-plus/test"; import * as z3 from "zod/v3"; import * as z4 from "zod/v4"; import { @@ -7,7 +14,7 @@ import { projects, CodeParameters, } from "./framework2"; -import { _internalGetGlobalState } from "./logger"; +import { _internalGetGlobalState } from "./logger/core"; import { configureNode } from "./node/config"; import { zodToJsonSchema } from "./zod/utils"; diff --git a/js/src/framework2.ts b/js/src/framework2.ts index 0db3c1a64..10b390c30 100644 --- a/js/src/framework2.ts +++ b/js/src/framework2.ts @@ -1,6 +1,6 @@ -import { _initializeSpanContext } from "./framework"; +import { _initializeSpanContext, getEvaluatorRegistry } from "./framework"; import type { Trace } from "./trace"; -import iso from "./isomorph"; +import iso from "#braintrust-platform"; import { slugify } from "../util/string_util"; import { z } from "zod/v3"; import { Project as projectSchema } from "./sdk-schemas"; @@ -14,12 +14,8 @@ import type { ExtendedSavedFunctionId, } from "./sdk-types"; import { loadPrettyXact, TransactionId } from "../util/index"; -import { - _internalGetGlobalState, - login, - Prompt, - PromptRowWithId, -} from "./logger"; +import { _internalGetGlobalState, login } from "./logger/core"; +import { Prompt, PromptRowWithId } from "./logger/prompts"; import type { BaseFnOpts, GenericFunction } from "./framework-types"; import type { EvalParameters } from "./eval-parameters"; import { @@ -49,11 +45,11 @@ type CreateProjectOpts = } | { id: string }; class ProjectBuilder { - create(opts: CreateProjectOpts) { + create(opts: CreateProjectOpts): Project { return new Project(opts); } } -export const projects = new ProjectBuilder(); +export const projects: ProjectBuilder = new ProjectBuilder(); class Project { public readonly name?: string; @@ -88,20 +84,18 @@ class Project { this.classifiers = new ClassifierBuilder(this); } - public addPrompt(prompt: CodePrompt) { + public addPrompt(prompt: CodePrompt): void { this._publishablePrompts.push(prompt); if (globalThis._lazy_load) { - globalThis._evals.prompts.push(prompt); + getEvaluatorRegistry().prompts.push(prompt); } } - public addParameters(parameters: CodeParameters) { + public addParameters(parameters: CodeParameters): void { this._publishableParameters.push(parameters); if (globalThis._lazy_load) { - if (globalThis._evals.parameters == null) - globalThis._evals.parameters = []; - - globalThis._evals.parameters.push(parameters); + const registry = getEvaluatorRegistry(); + (registry.parameters ??= []).push(parameters); } } @@ -114,14 +108,14 @@ class Project { // eslint-disable-next-line @typescript-eslint/no-explicit-any GenericFunction >, - ) { + ): void { this._publishableCodeFunctions.push(fn); if (globalThis._lazy_load) { - globalThis._evals.functions.push(fn); + getEvaluatorRegistry().functions.push(fn); } } - async publish() { + async publish(): Promise { if (globalThis._lazy_load) { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.warn("publish() is a no-op when running `bt push`."); @@ -233,7 +227,7 @@ class ScorerBuilder { Exact>, Returns >, - >(opts: ScorerOpts) { + >(opts: ScorerOpts): void { this.taskCounter++; let resolvedName = opts.name; @@ -313,7 +307,9 @@ class ClassifierBuilder { Exact>, Returns >, - >(opts: ClassifierOpts) { + >( + opts: ClassifierOpts, + ): CodeFunction>, Returns, Fn> { this.taskCounter++; let resolvedName = opts.name ?? opts.handler.name; diff --git a/js/src/functions/invoke.test.ts b/js/src/functions/invoke.test.ts index 4dd5deef6..022ba1a80 100644 --- a/js/src/functions/invoke.test.ts +++ b/js/src/functions/invoke.test.ts @@ -1,6 +1,7 @@ -import { describe, expect, test, beforeEach, afterEach } from "vitest"; +import { describe, expect, test, beforeEach, afterEach } from "vite-plus/test"; import { initFunction } from "./invoke"; -import { _internalGetGlobalState, _exportsForTestingOnly } from "../logger"; +import { _internalGetGlobalState } from "../logger/core"; +import { _exportsForTestingOnly } from "../logger/testing"; import { configureNode } from "../node/config"; describe("initFunction", () => { diff --git a/js/src/functions/invoke.ts b/js/src/functions/invoke.ts index ac6e3f1a4..fdc97f807 100644 --- a/js/src/functions/invoke.ts +++ b/js/src/functions/invoke.ts @@ -11,7 +11,7 @@ import { Exportable, FullLoginOptions, getSpanParentObject, -} from "../logger"; +} from "../logger/core"; import { BraintrustStream } from "./stream"; import { z } from "zod/v3"; @@ -279,7 +279,7 @@ export function initFunction({ slug: string; version?: string; state?: BraintrustState; -}) { +}): (input: any) => Promise { // Disable span cache since remote function spans won't be in the local cache const s = state ?? _internalGetGlobalState(); s?.spanCache?.disable(); diff --git a/js/src/functions/stream.test.ts b/js/src/functions/stream.test.ts index 235c5dbcb..d3585aa9e 100644 --- a/js/src/functions/stream.test.ts +++ b/js/src/functions/stream.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { BraintrustStream, BraintrustStreamChunk, diff --git a/js/src/git-command.test.ts b/js/src/git-command.test.ts index 0e483ed8a..0ea06ced6 100644 --- a/js/src/git-command.test.ts +++ b/js/src/git-command.test.ts @@ -1,5 +1,5 @@ import * as path from "node:path"; -import { afterEach, expect, test } from "vitest"; +import { afterEach, expect, test } from "vite-plus/test"; import { findGitExecutable, resetGitExecutableCacheForTests, diff --git a/js/src/git-command.ts b/js/src/git-command.ts index 58969cb50..4eec1ba4b 100644 --- a/js/src/git-command.ts +++ b/js/src/git-command.ts @@ -16,7 +16,7 @@ function executableSearchPath(): string | undefined { } export async function findGitExecutable( - searchPath = executableSearchPath(), + searchPath: string | undefined = executableSearchPath(), ): Promise { if (!searchPath) { return undefined; diff --git a/js/src/gitutil.ts b/js/src/gitutil.ts index 091404ce6..0f1f70422 100644 --- a/js/src/gitutil.ts +++ b/js/src/gitutil.ts @@ -105,7 +105,7 @@ async function getBaseBranchAncestor(remote: string | undefined = undefined) { export async function getPastNAncestors( n: number = 1000, remote: string | undefined = undefined, -) { +): Promise { const repoPath = await currentRepoPath(); if (!repoPath) { return []; @@ -150,7 +150,9 @@ function truncateToByteLimit(s: string, byteLimit: number = 65536): string { return new TextDecoder().decode(truncated); } -export async function getRepoInfo(settings?: GitMetadataSettings) { +export async function getRepoInfo( + settings?: GitMetadataSettings, +): Promise { if (!settings || settings.collect === "none") { return undefined; } diff --git a/js/src/global-instrumentation-hooks.test.ts b/js/src/global-instrumentation-hooks.test.ts index 687695850..5a192ada0 100644 --- a/js/src/global-instrumentation-hooks.test.ts +++ b/js/src/global-instrumentation-hooks.test.ts @@ -1,7 +1,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import { randomUUID } from "node:crypto"; import { tracingChannel } from "node:diagnostics_channel"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { GLOBAL_INSTRUMENTATION_HOOK_BRAND, GLOBAL_INSTRUMENTATION_HOOKS_KEY, diff --git a/js/src/id-gen.test.ts b/js/src/id-gen.test.ts index 2ea2ae85b..e21473663 100644 --- a/js/src/id-gen.test.ts +++ b/js/src/id-gen.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe, beforeEach, afterEach } from "vitest"; +import { expect, test, describe, beforeEach, afterEach } from "vite-plus/test"; import { UUIDGenerator, OTELIDGenerator, getIdGenerator } from "./id-gen"; import { configureNode } from "./node/config"; diff --git a/js/src/id-gen.ts b/js/src/id-gen.ts index e7243c020..1d6f4a425 100644 --- a/js/src/id-gen.ts +++ b/js/src/id-gen.ts @@ -7,7 +7,7 @@ import { v4 as uuidv4 } from "uuid"; import { debugLogger } from "./debug-logger"; -import iso from "./isomorph"; +import iso from "#braintrust-platform"; /** * Abstract base class for ID generators diff --git a/js/src/imports.test.ts b/js/src/imports.test.ts index e00c08223..a81e59237 100644 --- a/js/src/imports.test.ts +++ b/js/src/imports.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from "vite-plus/test"; import * as fs from "fs"; import * as path from "path"; diff --git a/js/src/instrumentation/auto-instrumentation-suppression.test.ts b/js/src/instrumentation/auto-instrumentation-suppression.test.ts index f3439ac2e..0dd9566bb 100644 --- a/js/src/instrumentation/auto-instrumentation-suppression.test.ts +++ b/js/src/instrumentation/auto-instrumentation-suppression.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vite-plus/test"; import { configureNode } from "../node/config"; import { isAutoInstrumentationSuppressed, diff --git a/js/src/instrumentation/auto-instrumentation-suppression.ts b/js/src/instrumentation/auto-instrumentation-suppression.ts index 8bb6b4fa7..c93af54aa 100644 --- a/js/src/instrumentation/auto-instrumentation-suppression.ts +++ b/js/src/instrumentation/auto-instrumentation-suppression.ts @@ -1,4 +1,4 @@ -import iso, { type IsoAsyncLocalStorage } from "../isomorph"; +import iso, { type IsoAsyncLocalStorage } from "#braintrust-platform"; let autoInstrumentationSuppressionStore: | IsoAsyncLocalStorage diff --git a/js/src/instrumentation/config.ts b/js/src/instrumentation/config.ts index 1ad3744f0..e230d7dc0 100644 --- a/js/src/instrumentation/config.ts +++ b/js/src/instrumentation/config.ts @@ -159,47 +159,6 @@ const envIntegrationAliases: Record< "@elevenlabs/elevenlabs-js": "elevenlabs", }; -export function getDefaultInstrumentationIntegrations(): Record< - keyof InstrumentationIntegrationsConfig, - boolean -> { - return { - openai: true, - openaiCodexSDK: true, - anthropic: true, - aisdk: true, - googleGenAI: true, - googleGenerativeAI: true, - googleADK: true, - huggingface: true, - claudeAgentSDK: true, - cloudflareAIChat: true, - cloudflareThink: true, - cursorSDK: true, - mastra: true, - openAIAgents: true, - openrouter: true, - openrouterAgent: true, - mistral: true, - ollama: true, - cohere: true, - groq: true, - awsBedrockRuntime: true, - genkit: true, - gitHubCopilot: true, - langchain: true, - langgraph: true, - langgraphSDK: true, - langsmith: true, - voyageai: true, - typesafe: true, - elevenlabs: true, - piCodingAgent: true, - strandsAgentSDK: true, - cloudflareAgents: true, - }; -} - export function readDisabledInstrumentationEnvConfig( disabledList: string | undefined, ): InstrumentationConfig { diff --git a/js/src/instrumentation/core/channel-definitions.test.ts b/js/src/instrumentation/core/channel-definitions.test.ts index 383bcdf74..d1d6db324 100644 --- a/js/src/instrumentation/core/channel-definitions.test.ts +++ b/js/src/instrumentation/core/channel-definitions.test.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vite-plus/test"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import { channel, defineChannels } from "./channel-definitions"; diff --git a/js/src/instrumentation/core/channel-definitions.ts b/js/src/instrumentation/core/channel-definitions.ts index 57626bde8..f515081a3 100644 --- a/js/src/instrumentation/core/channel-definitions.ts +++ b/js/src/instrumentation/core/channel-definitions.ts @@ -1,5 +1,5 @@ -import iso from "../../isomorph"; -import type { IsoTracingChannel } from "../../isomorph"; +import iso from "#braintrust-platform"; +import type { IsoTracingChannel } from "#braintrust-platform"; import type { SpanInstrumentationName } from "../../span-origin"; import type { AsyncEndEventWith, @@ -25,7 +25,7 @@ type ChannelTypeInfo< __chunk?: TChunk; }; -type ChannelSpec< +export type ChannelSpec< TArgs extends EventArguments, TResult, TExtra extends object = Record, @@ -216,13 +216,20 @@ export function channel(spec: { return spec as AnyChannelSpec; } -type MaterializedChannel = T["kind"] extends "async" - ? TypedAsyncChannel< - ChannelSpec, ResultOf, ExtraOf, ChunkOf, "async"> - > - : TypedSyncStreamChannel< - ChannelSpec, ResultOf, ExtraOf, ChunkOf, "sync-stream"> - >; +export type MaterializedChannel = + T["kind"] extends "async" + ? TypedAsyncChannel< + ChannelSpec, ResultOf, ExtraOf, ChunkOf, "async"> + > + : TypedSyncStreamChannel< + ChannelSpec< + ArgsOf, + ResultOf, + ExtraOf, + ChunkOf, + "sync-stream" + > + >; export function defineChannels( pkg: string, diff --git a/js/src/instrumentation/core/channel-tracing-utils.test.ts b/js/src/instrumentation/core/channel-tracing-utils.test.ts index a9c78f365..caf846cb9 100644 --- a/js/src/instrumentation/core/channel-tracing-utils.test.ts +++ b/js/src/instrumentation/core/channel-tracing-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { buildStartSpanArgs } from "./channel-tracing-utils"; describe("buildStartSpanArgs", () => { diff --git a/js/src/instrumentation/core/channel-tracing-utils.ts b/js/src/instrumentation/core/channel-tracing-utils.ts index edf7ea7c6..201077209 100644 --- a/js/src/instrumentation/core/channel-tracing-utils.ts +++ b/js/src/instrumentation/core/channel-tracing-utils.ts @@ -1,6 +1,6 @@ import type { ChannelSpanInfo, SpanInfoCarrier, StartEvent } from "./types"; import { debugLogger } from "../../debug-logger"; -import type { Span, StartSpanArgs } from "../../logger"; +import type { Span, StartSpanArgs } from "../../logger/core"; import { isObject, mergeDicts } from "../../util"; type ChannelConfigName = diff --git a/js/src/instrumentation/core/channel-tracing.test.ts b/js/src/instrumentation/core/channel-tracing.test.ts index bd8157e31..775bd2acd 100644 --- a/js/src/instrumentation/core/channel-tracing.test.ts +++ b/js/src/instrumentation/core/channel-tracing.test.ts @@ -6,15 +6,15 @@ import { expect, it, vi, -} from "vitest"; +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; import { - _exportsForTestingOnly, currentSpan, initLogger, NOOP_SPAN, type Span, type TestBackgroundLogger, -} from "../../logger"; +} from "../../logger/core"; import { configureNode } from "../../node/config"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/core/channel-tracing.ts b/js/src/instrumentation/core/channel-tracing.ts index 0416d4d33..a5efdeea7 100644 --- a/js/src/instrumentation/core/channel-tracing.ts +++ b/js/src/instrumentation/core/channel-tracing.ts @@ -1,7 +1,14 @@ import { debugLogger } from "../../debug-logger"; -import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; -import { _internalGetGlobalState, startSpan, withCurrent } from "../../logger"; -import type { Span } from "../../logger"; +import type { + IsoChannelHandlers, + IsoTracingChannel, +} from "#braintrust-platform"; +import { + _internalGetGlobalState, + startSpan, + withCurrent, +} from "../../logger/core"; +import type { Span } from "../../logger/core"; import { withSpanInstrumentationName, type SpanInstrumentationName, diff --git a/js/src/instrumentation/core/logging.test.ts b/js/src/instrumentation/core/logging.test.ts index 706d9bd50..9e086bc7b 100644 --- a/js/src/instrumentation/core/logging.test.ts +++ b/js/src/instrumentation/core/logging.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { toLoggedError } from "./logging"; describe("toLoggedError", () => { diff --git a/js/src/instrumentation/core/stream-patcher.test.ts b/js/src/instrumentation/core/stream-patcher.test.ts index d357169f1..8fc3e671f 100644 --- a/js/src/instrumentation/core/stream-patcher.test.ts +++ b/js/src/instrumentation/core/stream-patcher.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi } from "vite-plus/test"; import { isAsyncIterable, observeByteStream, diff --git a/js/src/instrumentation/index.test.ts b/js/src/instrumentation/index.test.ts index b51519344..86dd82fb6 100644 --- a/js/src/instrumentation/index.test.ts +++ b/js/src/instrumentation/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import * as instrumentation from "./index"; describe("instrumentation public API", () => { diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index 0c383cf2a..050fb2620 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -17,5 +17,5 @@ export { configureInstrumentation } from "./registry"; export type { InstrumentationConfig } from "./registry"; // Integration hook used by @braintrust/otel so core flushes also drain OTEL. -export { registerOtelFlush } from "../logger"; +export { registerOtelFlush } from "../logger/core"; export type { SpanCustomizer, SpanExportData } from "./config"; diff --git a/js/src/instrumentation/instrumentation-consumers.test.ts b/js/src/instrumentation/instrumentation-consumers.test.ts index 9499bff31..a48eecffa 100644 --- a/js/src/instrumentation/instrumentation-consumers.test.ts +++ b/js/src/instrumentation/instrumentation-consumers.test.ts @@ -1,4 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { configureInstrumentation } from "./registry"; +import { GLOBAL_INSTRUMENTATION_HOOKS_PROTOCOL_VERSION } from "../global-instrumentation-hooks"; +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; import type { InstrumentationIntegrationsConfig } from "./config"; const registrations = vi.hoisted(() => ({ @@ -161,6 +163,11 @@ const integrationCases: Array< describe("registerInstrumentationConsumers", () => { beforeEach(() => { vi.clearAllMocks(); + delete (globalThis as Record)[ + Symbol.for( + `braintrust.registry.global-hooks.v${GLOBAL_INSTRUMENTATION_HOOKS_PROTOCOL_VERSION}`, + ) + ]; }); it("registers every instrumentation consumer by default", () => { @@ -170,16 +177,20 @@ describe("registerInstrumentationConsumers", () => { expect(register).toHaveBeenCalledTimes(1); } expect(registrations.langSmith).toHaveBeenCalledWith({ - skipLangChainRuns: true, + skipLangChainRuns: expect.any(Function), }); + expect(registrations.langSmith.mock.calls[0][0].skipLangChainRuns()).toBe( + true, + ); }); it.each(integrationCases)( "does not register the %s integration when disabled", (integration, disabledRegistrations) => { - registerInstrumentationConsumers({ + configureInstrumentation({ integrations: { [integration]: false }, }); + registerInstrumentationConsumers(); for (const register of disabledRegistrations) { expect(register).not.toHaveBeenCalled(); @@ -190,19 +201,24 @@ describe("registerInstrumentationConsumers", () => { it.each([["langgraph", registrations.langChain]] as const)( "honors the related %s disable flag", (integration, register) => { - registerInstrumentationConsumers({ + configureInstrumentation({ integrations: { [integration]: false }, }); + registerInstrumentationConsumers(); expect(register).not.toHaveBeenCalled(); }, ); it("tells LangSmith not to suppress LangChain runs when LangChain is disabled", () => { - registerInstrumentationConsumers({ integrations: { langchain: false } }); + configureInstrumentation({ integrations: { langchain: false } }); + registerInstrumentationConsumers(); expect(registrations.langSmith).toHaveBeenCalledWith({ - skipLangChainRuns: false, + skipLangChainRuns: expect.any(Function), }); + expect(registrations.langSmith.mock.calls[0][0].skipLangChainRuns()).toBe( + false, + ); }); }); diff --git a/js/src/instrumentation/instrumentation-consumers.ts b/js/src/instrumentation/instrumentation-consumers.ts index 11667b87e..fd41914c3 100644 --- a/js/src/instrumentation/instrumentation-consumers.ts +++ b/js/src/instrumentation/instrumentation-consumers.ts @@ -1,173 +1,68 @@ -import { registerTypeSafeInstrumentation } from "./providers/typesafe-instrumentation"; -import { registerLangGraphSDKInstrumentation } from "./providers/langgraph-sdk-instrumentation"; -import { registerGoogleGenerativeAIInstrumentation } from "./providers/google-generative-ai-instrumentation"; -import { registerElevenLabsInstrumentation } from "./providers/elevenlabs-instrumentation"; -import { registerOpenAIInstrumentation } from "./providers/openai-instrumentation"; -import { registerOpenAICodexInstrumentation } from "./providers/openai-codex-instrumentation"; -import { registerAnthropicInstrumentation } from "./providers/anthropic-instrumentation"; -import { registerAISDKInstrumentation } from "./providers/ai-sdk-instrumentation"; -import { registerClaudeAgentSDKInstrumentation } from "./providers/claude-agent-sdk-instrumentation"; -import { registerCloudflareThinkInstrumentation } from "./providers/cloudflare-think-instrumentation"; -import { registerCursorSDKInstrumentation } from "./providers/cursor-sdk-instrumentation"; -import { registerOpenAIAgentsInstrumentation } from "./providers/openai-agents-instrumentation"; -import { registerGoogleGenAIInstrumentation } from "./providers/google-genai-instrumentation"; -import { registerHuggingFaceInstrumentation } from "./providers/huggingface-instrumentation"; -import { registerHuggingFaceTransformersInstrumentation } from "./providers/huggingface-transformers-instrumentation"; -import { registerOpenRouterAgentInstrumentation } from "./providers/openrouter-agent-instrumentation"; -import { registerOpenRouterInstrumentation } from "./providers/openrouter-instrumentation"; -import { registerMistralInstrumentation } from "./providers/mistral-instrumentation"; -import { registerOllamaInstrumentation } from "./providers/ollama-instrumentation"; -import { registerGoogleADKInstrumentation } from "./providers/google-adk-instrumentation"; -import { registerCohereInstrumentation } from "./providers/cohere-instrumentation"; -import { registerGroqInstrumentation } from "./providers/groq-instrumentation"; -import { registerBedrockRuntimeInstrumentation } from "./providers/bedrock-runtime-instrumentation"; -import { registerGenkitInstrumentation } from "./providers/genkit-instrumentation"; -import { registerGitHubCopilotInstrumentation } from "./providers/github-copilot-instrumentation"; -import { registerLangChainInstrumentation } from "./providers/langchain-instrumentation"; -import { registerLangSmithInstrumentation } from "./providers/langsmith-instrumentation"; -import { registerPiCodingAgentInstrumentation } from "./providers/pi-coding-agent-instrumentation"; -import { registerStrandsAgentSDKInstrumentation } from "./providers/strands-agent-sdk-instrumentation"; -import { registerVoyageAIInstrumentation } from "./providers/voyageai-instrumentation"; -import { registerCloudflareAIChatInstrumentation } from "./providers/cloudflare-ai-chat-consumer"; -import { registerCloudflareAgentsInstrumentation } from "./providers/cloudflare-agents-instrumentation"; -import type { InstrumentationConfig } from "./config"; - -/** Registers the configured process-lifetime instrumentation consumers. */ -export function registerInstrumentationConsumers( - config: InstrumentationConfig = {}, -): void { - const integrations = config.integrations ?? {}; - - if (integrations.openai !== false) { - registerOpenAIInstrumentation(); - } - - if (integrations.openaiCodexSDK !== false) { - registerOpenAICodexInstrumentation(); - } - - if (integrations.anthropic !== false) { - registerAnthropicInstrumentation(); - } - - if (integrations.aisdk !== false) { - registerAISDKInstrumentation(); - } - - if (integrations.claudeAgentSDK !== false) { - registerClaudeAgentSDKInstrumentation(); - } - - if (integrations.cloudflareThink !== false) { - registerCloudflareThinkInstrumentation(); - } - - if (integrations.cursorSDK !== false) { - registerCursorSDKInstrumentation(); - } - - if (integrations.openAIAgents !== false) { - registerOpenAIAgentsInstrumentation(); - } - - if (integrations.googleGenAI !== false) { - registerGoogleGenAIInstrumentation(); - } - - if (integrations.huggingface !== false) { - registerHuggingFaceInstrumentation(); - registerHuggingFaceTransformersInstrumentation(); - } - - if (integrations.openrouter !== false) { - registerOpenRouterInstrumentation(); - } - - if (integrations.openrouterAgent !== false) { - registerOpenRouterAgentInstrumentation(); - } - - if (integrations.mistral !== false) { - registerMistralInstrumentation(); - } - - if (integrations.ollama !== false) { - registerOllamaInstrumentation(); - } - - if (integrations.googleADK !== false) { - registerGoogleADKInstrumentation(); - } - - if (integrations.cohere !== false) { - registerCohereInstrumentation(); - } - - if (integrations.voyageai !== false) { - registerVoyageAIInstrumentation(); - } - - if (integrations.groq !== false) { - registerGroqInstrumentation(); - } - - if (integrations.awsBedrockRuntime !== false) { - registerBedrockRuntimeInstrumentation(); - } - - if (integrations.genkit !== false) { - registerGenkitInstrumentation(); - } - - if (integrations.gitHubCopilot !== false) { - registerGitHubCopilotInstrumentation(); - } - - if (integrations.piCodingAgent !== false) { - registerPiCodingAgentInstrumentation(); - } - - if (integrations.strandsAgentSDK !== false) { - registerStrandsAgentSDKInstrumentation(); - } - - if (integrations.cloudflareAIChat !== false) { - registerCloudflareAIChatInstrumentation(); - } - - if (integrations.cloudflareAgents !== false) { - registerCloudflareAgentsInstrumentation(); - } - - if (integrations.langchain !== false && integrations.langgraph !== false) { - registerLangChainInstrumentation(); - } - - if (integrations.langsmith !== false) { - registerLangSmithInstrumentation({ - skipLangChainRuns: integrations.langchain !== false, - }); - } - - if (integrations.elevenlabs !== false) { - registerElevenLabsInstrumentation(); - } - - if (integrations.googleGenerativeAI !== false) { - registerGoogleGenerativeAIInstrumentation(); - } - - if (integrations.langgraphSDK !== false) { - registerLangGraphSDKInstrumentation(); - } - - if (integrations.typesafe !== false) { - registerTypeSafeInstrumentation(); - } - - // Mastra is intentionally not wired here: `@mastra/core` ships its own - // ObservabilityExporter contract, and `BraintrustObservabilityExporter` - // (wrappers/mastra.ts) is auto-installed by the loader patch in - // `auto-instrumentations/loader/mastra-observability-patch.ts` rather than - // by an instrumentation consumer / global hook subscription. +import { registerInstrumentation as openai } from "./registrations/openai"; +import { registerInstrumentation as openaiCodexSDK } from "./registrations/openaiCodexSDK"; +import { registerInstrumentation as anthropic } from "./registrations/anthropic"; +import { registerInstrumentation as aisdk } from "./registrations/aisdk"; +import { registerInstrumentation as claudeAgentSDK } from "./registrations/claudeAgentSDK"; +import { registerInstrumentation as cloudflareThink } from "./registrations/cloudflareThink"; +import { registerInstrumentation as cursorSDK } from "./registrations/cursorSDK"; +import { registerInstrumentation as openAIAgents } from "./registrations/openAIAgents"; +import { registerInstrumentation as googleGenAI } from "./registrations/googleGenAI"; +import { registerInstrumentation as huggingface } from "./registrations/huggingface"; +import { registerInstrumentation as openrouter } from "./registrations/openrouter"; +import { registerInstrumentation as openrouterAgent } from "./registrations/openrouterAgent"; +import { registerInstrumentation as mistral } from "./registrations/mistral"; +import { registerInstrumentation as ollama } from "./registrations/ollama"; +import { registerInstrumentation as googleADK } from "./registrations/googleADK"; +import { registerInstrumentation as cohere } from "./registrations/cohere"; +import { registerInstrumentation as voyageai } from "./registrations/voyageai"; +import { registerInstrumentation as groq } from "./registrations/groq"; +import { registerInstrumentation as awsBedrockRuntime } from "./registrations/awsBedrockRuntime"; +import { registerInstrumentation as genkit } from "./registrations/genkit"; +import { registerInstrumentation as gitHubCopilot } from "./registrations/gitHubCopilot"; +import { registerInstrumentation as piCodingAgent } from "./registrations/piCodingAgent"; +import { registerInstrumentation as strandsAgentSDK } from "./registrations/strandsAgentSDK"; +import { registerInstrumentation as cloudflareAIChat } from "./registrations/cloudflareAIChat"; +import { registerInstrumentation as cloudflareAgents } from "./registrations/cloudflareAgents"; +import { registerInstrumentation as langchain } from "./registrations/langchain"; +import { registerInstrumentation as langsmith } from "./registrations/langsmith"; +import { registerInstrumentation as elevenlabs } from "./registrations/elevenlabs"; +import { registerInstrumentation as googleGenerativeAI } from "./registrations/googleGenerativeAI"; +import { registerInstrumentation as langgraphSDK } from "./registrations/langgraphSDK"; +import { registerInstrumentation as typesafe } from "./registrations/typesafe"; +import { registerInstrumentation as mastra } from "./registrations/mastra"; + +/** Register every enabled provider. Only explicit auto setup imports this module. */ +export function registerInstrumentationConsumers(): void { + openai(); + openaiCodexSDK(); + anthropic(); + aisdk(); + claudeAgentSDK(); + cloudflareThink(); + cursorSDK(); + openAIAgents(); + googleGenAI(); + huggingface(); + openrouter(); + openrouterAgent(); + mistral(); + ollama(); + googleADK(); + cohere(); + voyageai(); + groq(); + awsBedrockRuntime(); + genkit(); + gitHubCopilot(); + piCodingAgent(); + strandsAgentSDK(); + cloudflareAIChat(); + cloudflareAgents(); + langchain(); + langsmith(); + elevenlabs(); + googleGenerativeAI(); + langgraphSDK(); + typesafe(); + mastra(); } diff --git a/js/src/instrumentation/providers/ai-sdk-channels.ts b/js/src/instrumentation/providers/ai-sdk-channels.ts index c78fe5ca6..b4f73d452 100644 --- a/js/src/instrumentation/providers/ai-sdk-channels.ts +++ b/js/src/instrumentation/providers/ai-sdk-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { ChannelSpanInfo } from "../core/types"; @@ -36,217 +40,427 @@ type AISDKModelChannelContext = { model: AISDKLanguageModel; }; -export const BRAINTRUST_WRAPPED_AI_SDK_MODEL = Symbol.for( - "braintrust.ai-sdk.wrapped-model", -); +export const BRAINTRUST_WRAPPED_AI_SDK_MODEL: symbol = /* @__PURE__ */ (() => + Symbol.for("braintrust.ai-sdk.wrapped-model"))(); -export const aiSDKChannels = defineChannels( - "ai", - { - modelGenerate: channel< +export const aiSDKChannels: { + modelGenerate: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKResult, - AISDKModelChannelContext - >({ - channelName: "model.doGenerate", - kind: "async", - }), - modelStream: channel< + AISDKModelChannelContext, + never, + "async" + > + >; + modelStream: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKResult & { stream: ReadableStream }, - AISDKModelChannelContext - >({ - channelName: "model.doStream", - kind: "async", - }), - evaluate: channel< + AISDKModelChannelContext, + never, + "async" + > + >; + evaluate: MaterializedChannel< + ChannelSpec< [AISDKEvaluateParams], AISDKEvaluationResult, - AISDKChannelContext - >({ - channelName: "evaluate", - kind: "async", - }), - generateText: channel< + AISDKChannelContext, + never, + "async" + > + >; + generateText: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "generateText", - kind: "async", - }), - generateImage: channel< + unknown, + "async" + > + >; + generateImage: MaterializedChannel< + ChannelSpec< [AISDKGenerateImageParams], AISDKResult, - AISDKChannelContext - >({ - channelName: "generateImage", - kind: "async", - }), - streamTextSync: channel< + AISDKChannelContext, + never, + "async" + > + >; + streamTextSync: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKResult, AISDKChannelContext, - unknown - >({ - channelName: "streamText.sync", - kind: "sync-stream", - }), - generateObject: channel< + unknown, + "sync-stream" + > + >; + generateObject: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "generateObject", - kind: "async", - }), - streamObjectSync: channel< + unknown, + "async" + > + >; + streamObjectSync: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKResult, AISDKChannelContext, - unknown - >({ - channelName: "streamObject.sync", - kind: "sync-stream", - }), - embed: channel< + unknown, + "sync-stream" + > + >; + embed: MaterializedChannel< + ChannelSpec< [AISDKEmbedParams], AISDKEmbeddingResult, - AISDKChannelContext - >({ - channelName: "embed", - kind: "async", - }), - embedMany: channel< + AISDKChannelContext, + never, + "async" + > + >; + embedMany: MaterializedChannel< + ChannelSpec< [AISDKEmbedParams], AISDKEmbeddingResult, - AISDKChannelContext - >({ - channelName: "embedMany", - kind: "async", - }), - rerank: channel< + AISDKChannelContext, + never, + "async" + > + >; + rerank: MaterializedChannel< + ChannelSpec< [AISDKRerankParams], AISDKRerankResult, - AISDKChannelContext - >({ - channelName: "rerank", - kind: "async", - }), - agentGenerate: channel< + AISDKChannelContext, + never, + "async" + > + >; + agentGenerate: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "Agent.generate", - kind: "async", - }), - agentStream: channel< + unknown, + "async" + > + >; + agentStream: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "Agent.stream", - kind: "async", - }), - agentStreamSync: channel< + unknown, + "async" + > + >; + agentStreamSync: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKResult, AISDKChannelContext, - unknown - >({ - channelName: "Agent.stream.sync", - kind: "sync-stream", - }), - toolLoopAgentGenerate: channel< + unknown, + "sync-stream" + > + >; + toolLoopAgentGenerate: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "ToolLoopAgent.generate", - kind: "async", - }), - toolLoopAgentStream: channel< + unknown, + "async" + > + >; + toolLoopAgentStream: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "ToolLoopAgent.stream", - kind: "async", - }), - workflowAgentStream: channel< + unknown, + "async" + > + >; + workflowAgentStream: MaterializedChannel< + ChannelSpec< [AISDKCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "WorkflowAgent.stream", - kind: "async", - }), - v7CreateTelemetryDispatcher: channel< + unknown, + "async" + > + >; + v7CreateTelemetryDispatcher: MaterializedChannel< + ChannelSpec< [AISDKV7CreateTelemetryDispatcherArgs], - AISDKV7TelemetryDispatcher - >({ - channelName: "createTelemetryDispatcher", - kind: "sync-stream", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.AI_SDK }, -); + AISDKV7TelemetryDispatcher, + Record, + never, + "sync-stream" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "ai", + { + modelGenerate: channel< + [AISDKCallParams], + AISDKResult, + AISDKModelChannelContext + >({ + channelName: "model.doGenerate", + kind: "async", + }), + modelStream: channel< + [AISDKCallParams], + AISDKResult & { stream: ReadableStream }, + AISDKModelChannelContext + >({ + channelName: "model.doStream", + kind: "async", + }), + evaluate: channel< + [AISDKEvaluateParams], + AISDKEvaluationResult, + AISDKChannelContext + >({ + channelName: "evaluate", + kind: "async", + }), + generateText: channel< + [AISDKCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "generateText", + kind: "async", + }), + generateImage: channel< + [AISDKGenerateImageParams], + AISDKResult, + AISDKChannelContext + >({ + channelName: "generateImage", + kind: "async", + }), + streamTextSync: channel< + [AISDKCallParams], + AISDKResult, + AISDKChannelContext, + unknown + >({ + channelName: "streamText.sync", + kind: "sync-stream", + }), + generateObject: channel< + [AISDKCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "generateObject", + kind: "async", + }), + streamObjectSync: channel< + [AISDKCallParams], + AISDKResult, + AISDKChannelContext, + unknown + >({ + channelName: "streamObject.sync", + kind: "sync-stream", + }), + embed: channel< + [AISDKEmbedParams], + AISDKEmbeddingResult, + AISDKChannelContext + >({ + channelName: "embed", + kind: "async", + }), + embedMany: channel< + [AISDKEmbedParams], + AISDKEmbeddingResult, + AISDKChannelContext + >({ + channelName: "embedMany", + kind: "async", + }), + rerank: channel< + [AISDKRerankParams], + AISDKRerankResult, + AISDKChannelContext + >({ + channelName: "rerank", + kind: "async", + }), + agentGenerate: channel< + [AISDKCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "Agent.generate", + kind: "async", + }), + agentStream: channel< + [AISDKCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "Agent.stream", + kind: "async", + }), + agentStreamSync: channel< + [AISDKCallParams], + AISDKResult, + AISDKChannelContext, + unknown + >({ + channelName: "Agent.stream.sync", + kind: "sync-stream", + }), + toolLoopAgentGenerate: channel< + [AISDKCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "ToolLoopAgent.generate", + kind: "async", + }), + toolLoopAgentStream: channel< + [AISDKCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "ToolLoopAgent.stream", + kind: "async", + }), + workflowAgentStream: channel< + [AISDKCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "WorkflowAgent.stream", + kind: "async", + }), + v7CreateTelemetryDispatcher: channel< + [AISDKV7CreateTelemetryDispatcherArgs], + AISDKV7TelemetryDispatcher + >({ + channelName: "createTelemetryDispatcher", + kind: "sync-stream", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.AI_SDK }, + ))(); -export const harnessAgentChannels = defineChannels( - "@ai-sdk/harness", - { - createSession: channel< +export const harnessAgentChannels: { + createSession: MaterializedChannel< + ChannelSpec< [AISDKHarnessAgentCreateSessionParams?], AISDKHarnessAgentSession, - AISDKChannelContext - >({ - channelName: "HarnessAgent.createSession", - kind: "async", - }), - generate: channel< + AISDKChannelContext, + never, + "async" + > + >; + generate: MaterializedChannel< + ChannelSpec< [AISDKHarnessAgentCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "HarnessAgent.generate", - kind: "async", - }), - stream: channel< + unknown, + "async" + > + >; + stream: MaterializedChannel< + ChannelSpec< [AISDKHarnessAgentCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "HarnessAgent.stream", - kind: "async", - }), - continueGenerate: channel< + unknown, + "async" + > + >; + continueGenerate: MaterializedChannel< + ChannelSpec< [AISDKHarnessAgentCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "HarnessAgent.continueGenerate", - kind: "async", - }), - continueStream: channel< + unknown, + "async" + > + >; + continueStream: MaterializedChannel< + ChannelSpec< [AISDKHarnessAgentCallParams], AISDKStreamResult, AISDKChannelContext, - unknown - >({ - channelName: "HarnessAgent.continueStream", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.AI_SDK }, -); + unknown, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@ai-sdk/harness", + { + createSession: channel< + [AISDKHarnessAgentCreateSessionParams?], + AISDKHarnessAgentSession, + AISDKChannelContext + >({ + channelName: "HarnessAgent.createSession", + kind: "async", + }), + generate: channel< + [AISDKHarnessAgentCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "HarnessAgent.generate", + kind: "async", + }), + stream: channel< + [AISDKHarnessAgentCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "HarnessAgent.stream", + kind: "async", + }), + continueGenerate: channel< + [AISDKHarnessAgentCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "HarnessAgent.continueGenerate", + kind: "async", + }), + continueStream: channel< + [AISDKHarnessAgentCallParams], + AISDKStreamResult, + AISDKChannelContext, + unknown + >({ + channelName: "HarnessAgent.continueStream", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.AI_SDK }, + ))(); diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts index 04ecd0c04..c8aa9515d 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.streaming.test.ts @@ -7,14 +7,11 @@ import { describe, expect, test, -} from "vitest"; +} from "vite-plus/test"; import * as ai from "ai"; import { configureNode } from "../../node/config"; -import { - _exportsForTestingOnly, - initLogger, - TestBackgroundLogger, -} from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger, TestBackgroundLogger } from "../../logger/core"; import { wrapAISDK, wrapAgentClass } from "../../wrappers/ai-sdk"; import { captureHarnessCreateSessionParent, diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts index 144bb11c0..f6dfbcdce 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vite-plus/test"; const telemetryMocks = vi.hoisted(() => ({ braintrustAISDKTelemetry: vi.fn(), @@ -12,12 +12,16 @@ const telemetryMocks = vi.hoisted(() => ({ })); // Mock iso's newTracingChannel - must be before any imports that use it -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(), getEnv: vi.fn((name: string) => name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, ), + newAsyncLocalStorage: () => ({ + getStore: () => undefined, + run: (_store: unknown, callback: () => unknown) => callback(), + }), }, })); @@ -37,7 +41,7 @@ import { extractTokenMetrics, serializeModelWithProvider, } from "./ai-sdk-instrumentation"; -import iso from "../../isomorph"; +import iso from "#braintrust-platform"; import { serializeAISDKToolsForLogging } from "../../wrappers/ai-sdk/tool-serialization"; import { BRAINTRUST_AI_SDK_V7_OPERATION_KEY as AI_SDK_V7_OPERATION_KEY } from "../../vendor-sdk-types/ai-sdk-v7-telemetry"; diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.ts index 1566e8f73..4691ad632 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.ts @@ -7,7 +7,7 @@ import { } from "../core/channel-tracing"; import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; -import type { IsoChannelHandlers } from "../../isomorph"; +import type { IsoChannelHandlers } from "#braintrust-platform"; import { SpanTypeAttribute, isObject, @@ -25,7 +25,7 @@ import { startSpan, type Span, withCurrent, -} from "../../logger"; +} from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -1658,9 +1658,9 @@ const convertDataToAttachment = ( } else if (typeof data === "string" && data.length > 0) { blob = convertDataToBlob(data, mimeType); } else if (data instanceof Uint8Array) { - blob = new Blob([data], { type: mimeType }); + blob = new Blob([Uint8Array.from(data)], { type: mimeType }); } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) { - blob = new Blob([data], { type: mimeType }); + blob = new Blob([Uint8Array.from(data)], { type: mimeType }); } else if (data instanceof Blob) { blob = data; } diff --git a/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts b/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts index 6c6bbffd7..e33ae78bb 100644 --- a/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts @@ -1,12 +1,19 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; import { Attachment, - _exportsForTestingOnly, currentSpan, initLogger, startSpan, withCurrent, -} from "../../logger"; +} from "../../logger/core"; import { configureNode } from "../../node/config"; import { braintrustAISDKTelemetry } from "../../wrappers/ai-sdk/telemetry"; import { diff --git a/js/src/instrumentation/providers/anthropic-channels.ts b/js/src/instrumentation/providers/anthropic-channels.ts index e1a8793ec..eb83e8b1c 100644 --- a/js/src/instrumentation/providers/anthropic-channels.ts +++ b/js/src/instrumentation/providers/anthropic-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -15,52 +19,99 @@ import type { type AnthropicResult = AnthropicMessage | AnthropicMessageStream; -export const anthropicChannels = defineChannels( - "@anthropic-ai/sdk", - { - messagesCreate: channel< +export const anthropicChannels: { + messagesCreate: MaterializedChannel< + ChannelSpec< [AnthropicCreateParams], AnthropicResult, Record, - AnthropicStreamEvent - >({ - channelName: "messages.create", - kind: "async", - }), - betaMessagesCreate: channel< + AnthropicStreamEvent, + "async" + > + >; + betaMessagesCreate: MaterializedChannel< + ChannelSpec< [AnthropicCreateParams], AnthropicResult, Record, - AnthropicStreamEvent - >({ - channelName: "beta.messages.create", - kind: "async", - }), - betaMessagesToolRunner: channel< + AnthropicStreamEvent, + "async" + > + >; + betaMessagesToolRunner: MaterializedChannel< + ChannelSpec< [AnthropicToolRunnerParams], - AnthropicToolRunner - >({ - channelName: "beta.messages.toolRunner", - kind: "sync-stream", - }), - betaSessionsEventsStream: channel< + AnthropicToolRunner, + Record, + never, + "sync-stream" + > + >; + betaSessionsEventsStream: MaterializedChannel< + ChannelSpec< [string, AnthropicSessionEventStreamParams?], AnthropicSessionEventStream, Record, - AnthropicSessionEvent - >({ - channelName: "beta.sessions.events.stream", - kind: "async", - }), - betaSessionsThreadsEventsStream: channel< + AnthropicSessionEvent, + "async" + > + >; + betaSessionsThreadsEventsStream: MaterializedChannel< + ChannelSpec< [string, AnthropicSessionThreadEventStreamParams], AnthropicSessionEventStream, Record, - AnthropicSessionEvent - >({ - channelName: "beta.sessions.threads.events.stream", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.ANTHROPIC }, -); + AnthropicSessionEvent, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@anthropic-ai/sdk", + { + messagesCreate: channel< + [AnthropicCreateParams], + AnthropicResult, + Record, + AnthropicStreamEvent + >({ + channelName: "messages.create", + kind: "async", + }), + betaMessagesCreate: channel< + [AnthropicCreateParams], + AnthropicResult, + Record, + AnthropicStreamEvent + >({ + channelName: "beta.messages.create", + kind: "async", + }), + betaMessagesToolRunner: channel< + [AnthropicToolRunnerParams], + AnthropicToolRunner + >({ + channelName: "beta.messages.toolRunner", + kind: "sync-stream", + }), + betaSessionsEventsStream: channel< + [string, AnthropicSessionEventStreamParams?], + AnthropicSessionEventStream, + Record, + AnthropicSessionEvent + >({ + channelName: "beta.sessions.events.stream", + kind: "async", + }), + betaSessionsThreadsEventsStream: channel< + [string, AnthropicSessionThreadEventStreamParams], + AnthropicSessionEventStream, + Record, + AnthropicSessionEvent + >({ + channelName: "beta.sessions.threads.events.stream", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.ANTHROPIC }, + ))(); diff --git a/js/src/instrumentation/providers/anthropic-instrumentation.test.ts b/js/src/instrumentation/providers/anthropic-instrumentation.test.ts index c945c1f2e..3ffcb6245 100644 --- a/js/src/instrumentation/providers/anthropic-instrumentation.test.ts +++ b/js/src/instrumentation/providers/anthropic-instrumentation.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi } from "vite-plus/test"; // Mock iso's newTracingChannel - must be before any imports that use it -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(), getEnv: vi.fn((name: string) => @@ -16,7 +16,7 @@ import { processAttachmentsInInput, coalesceInput, } from "./anthropic-instrumentation"; -import { Attachment } from "../../logger"; +import { Attachment } from "../../logger/core"; const parseMetricsFromUsageForTest = (usage: unknown) => parseMetricsFromUsage(usage as any); @@ -24,8 +24,8 @@ const aggregateAnthropicStreamChunksForTest = (chunks: unknown[]) => aggregateAnthropicStreamChunks(chunks as any); // Mock startSpan from logger -vi.mock("../../logger", async () => { - const { default: iso } = await import("../../isomorph"); +vi.mock("../../logger/core", async () => { + const { default: iso } = await import("#braintrust-platform"); return { _internalCaptureAttachmentsEnabled: () => iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", diff --git a/js/src/instrumentation/providers/anthropic-instrumentation.ts b/js/src/instrumentation/providers/anthropic-instrumentation.ts index 0b5f3f112..925b73288 100644 --- a/js/src/instrumentation/providers/anthropic-instrumentation.ts +++ b/js/src/instrumentation/providers/anthropic-instrumentation.ts @@ -7,14 +7,17 @@ import { Attachment, startSpan as startBaseSpan, withCurrent, -} from "../../logger"; -import type { Span } from "../../logger"; +} from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, } from "../../span-origin"; import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; +import type { + IsoChannelHandlers, + IsoTracingChannel, +} from "#braintrust-platform"; import { debugLogger } from "../../debug-logger"; import { SpanTypeAttribute, diff --git a/js/src/instrumentation/providers/anthropic-sessions-instrumentation.test.ts b/js/src/instrumentation/providers/anthropic-sessions-instrumentation.test.ts index 0feffc9c6..e34e7ea90 100644 --- a/js/src/instrumentation/providers/anthropic-sessions-instrumentation.test.ts +++ b/js/src/instrumentation/providers/anthropic-sessions-instrumentation.test.ts @@ -1,11 +1,18 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; const { mockStartSpan, mockWithCurrent } = vi.hoisted(() => ({ mockStartSpan: vi.fn(), mockWithCurrent: vi.fn(), })); -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { getEnv: vi.fn(), newAsyncLocalStorage: vi.fn(() => ({ @@ -16,7 +23,7 @@ vi.mock("../../isomorph", () => ({ }, })); -vi.mock("../../logger", async (importOriginal) => { +vi.mock("../../logger/core", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, @@ -25,7 +32,7 @@ vi.mock("../../logger", async (importOriginal) => { }; }); -import iso from "../../isomorph"; +import iso from "#braintrust-platform"; import { collectAnthropicSession } from "../../wrappers/anthropic-session-collector"; import { registerAnthropicInstrumentation } from "./anthropic-instrumentation"; diff --git a/js/src/instrumentation/providers/bedrock-runtime-channels.ts b/js/src/instrumentation/providers/bedrock-runtime-channels.ts index 080b21a3a..8e32eff97 100644 --- a/js/src/instrumentation/providers/bedrock-runtime-channels.ts +++ b/js/src/instrumentation/providers/bedrock-runtime-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -12,36 +16,70 @@ type BedrockRuntimeStreamEvent = | BedrockRuntimeConverseStreamEvent | BedrockRuntimeResponseStreamEvent; -const clientSendChannel = channel< - [BedrockRuntimeCommandLike, unknown?], - BedrockRuntimeSendResult, - BedrockRuntimeChannelContext, - BedrockRuntimeStreamEvent ->({ - channelName: "client.send", - kind: "async", -}); +const clientSendChannel = /* @__PURE__ */ (() => + channel< + [BedrockRuntimeCommandLike, unknown?], + BedrockRuntimeSendResult, + BedrockRuntimeChannelContext, + BedrockRuntimeStreamEvent + >({ + channelName: "client.send", + kind: "async", + }))(); -export const bedrockRuntimeChannels = defineChannels( - "aws-bedrock-runtime", - { - clientSend: clientSendChannel, - }, - { instrumentationName: INSTRUMENTATION_NAMES.BEDROCK_RUNTIME }, -); +export const bedrockRuntimeChannels: { + clientSend: MaterializedChannel< + ChannelSpec< + [BedrockRuntimeCommandLike, unknown?], + BedrockRuntimeSendResult, + BedrockRuntimeChannelContext, + BedrockRuntimeStreamEvent, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "aws-bedrock-runtime", + { + clientSend: clientSendChannel, + }, + { instrumentationName: INSTRUMENTATION_NAMES.BEDROCK_RUNTIME }, + ))(); -export const smithyCoreChannels = defineChannels( - "@smithy/core", - { - clientSend: clientSendChannel, - }, - { instrumentationName: INSTRUMENTATION_NAMES.BEDROCK_RUNTIME }, -); +export const smithyCoreChannels: { + clientSend: MaterializedChannel< + ChannelSpec< + [BedrockRuntimeCommandLike, unknown?], + BedrockRuntimeSendResult, + BedrockRuntimeChannelContext, + BedrockRuntimeStreamEvent, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@smithy/core", + { + clientSend: clientSendChannel, + }, + { instrumentationName: INSTRUMENTATION_NAMES.BEDROCK_RUNTIME }, + ))(); -export const smithyClientChannels = defineChannels( - "@smithy/smithy-client", - { - clientSend: clientSendChannel, - }, - { instrumentationName: INSTRUMENTATION_NAMES.BEDROCK_RUNTIME }, -); +export const smithyClientChannels: { + clientSend: MaterializedChannel< + ChannelSpec< + [BedrockRuntimeCommandLike, unknown?], + BedrockRuntimeSendResult, + BedrockRuntimeChannelContext, + BedrockRuntimeStreamEvent, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@smithy/smithy-client", + { + clientSend: clientSendChannel, + }, + { instrumentationName: INSTRUMENTATION_NAMES.BEDROCK_RUNTIME }, + ))(); diff --git a/js/src/instrumentation/providers/bedrock-runtime-instrumentation.test.ts b/js/src/instrumentation/providers/bedrock-runtime-instrumentation.test.ts index b22906083..4f7a301a4 100644 --- a/js/src/instrumentation/providers/bedrock-runtime-instrumentation.test.ts +++ b/js/src/instrumentation/providers/bedrock-runtime-instrumentation.test.ts @@ -1,5 +1,14 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { registerInstrumentation } from "../registrations/awsBedrockRuntime"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger } from "../../logger/core"; import { configureNode } from "../../node/config"; import { smithyClientChannels, @@ -13,6 +22,7 @@ import { try { configureNode(); + registerInstrumentation(); } catch { // Best-effort initialization for test environments. } diff --git a/js/src/instrumentation/providers/bedrock-runtime-instrumentation.ts b/js/src/instrumentation/providers/bedrock-runtime-instrumentation.ts index dfd5a47be..4489dd123 100644 --- a/js/src/instrumentation/providers/bedrock-runtime-instrumentation.ts +++ b/js/src/instrumentation/providers/bedrock-runtime-instrumentation.ts @@ -2,7 +2,7 @@ import { traceStreamingChannel } from "../core/channel-tracing"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import { SpanTypeAttribute, isObject } from "../../../util/index"; import { getCurrentUnixTimestamp } from "../../util"; -import type { Span } from "../../logger"; +import type { Span } from "../../logger/core"; import type { AnyAsyncChannel } from "../core/channel-definitions"; import type { BedrockRuntimeConverseRequest, diff --git a/js/src/instrumentation/providers/claude-agent-sdk-channels.ts b/js/src/instrumentation/providers/claude-agent-sdk-channels.ts index c58e23144..e309dc035 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-channels.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -5,18 +9,29 @@ import type { ClaudeAgentSDKQueryParams, } from "../../vendor-sdk-types/claude-agent-sdk"; -export const claudeAgentSDKChannels = defineChannels( - "@anthropic-ai/claude-agent-sdk", - { - query: channel< +export const claudeAgentSDKChannels: { + query: MaterializedChannel< + ChannelSpec< [ClaudeAgentSDKQueryParams], AsyncIterable, Record, - ClaudeAgentSDKMessage - >({ - channelName: "query", - kind: "sync-stream", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.CLAUDE_AGENT_SDK }, -); + ClaudeAgentSDKMessage, + "sync-stream" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@anthropic-ai/claude-agent-sdk", + { + query: channel< + [ClaudeAgentSDKQueryParams], + AsyncIterable, + Record, + ClaudeAgentSDKMessage + >({ + channelName: "query", + kind: "sync-stream", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.CLAUDE_AGENT_SDK }, + ))(); diff --git a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.streaming.test.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.streaming.test.ts index bb8337651..608ee29a6 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.streaming.test.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.streaming.test.ts @@ -1,10 +1,14 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import type { BackgroundLogEvent } from "../../../util/index"; import { - _exportsForTestingOnly, - initLogger, - type TestBackgroundLogger, -} from "../../logger"; + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; +import type { BackgroundLogEvent } from "../../../util/index"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger, type TestBackgroundLogger } from "../../logger/core"; import { configureNode } from "../../node/config"; import type { ClaudeAgentSDKMessage, diff --git a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts index 9808eeaf3..d26a6de5e 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.test.ts @@ -1,4 +1,11 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + describe, + it, + expect, + vi, + beforeEach, + afterEach, +} from "vite-plus/test"; import { AsyncLocalStorage } from "node:async_hooks"; // Mock iso's newTracingChannel - must be before any imports that use it @@ -11,7 +18,7 @@ const streamPatcherMock = vi.hoisted(() => ({ | undefined, })); -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newAsyncLocalStorage: () => new AsyncLocalStorage(), newTracingChannel: vi.fn(), @@ -33,13 +40,13 @@ vi.mock("../core/stream-patcher", () => ({ })); import { registerClaudeAgentSDKInstrumentation } from "./claude-agent-sdk-instrumentation"; -import iso from "../../isomorph"; -import { _internalStartSpan as startSpan } from "../../logger"; +import iso from "#braintrust-platform"; +import { _internalStartSpan as startSpan } from "../../logger/core"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; // Mock the logger module -vi.mock("../../logger", () => { +vi.mock("../../logger/core", () => { const startSpan = vi.fn(() => ({ log: vi.fn(), end: vi.fn(), diff --git a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts index 2a0717733..5da5118a8 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-instrumentation.ts @@ -1,7 +1,7 @@ import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import { debugLogger } from "../../debug-logger"; -import { _internalStartSpan as startBaseSpan } from "../../logger"; -import type { Span } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/claude-agent-sdk-local-tool-context.ts b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-context.ts index e0c2129c3..597479368 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-local-tool-context.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-context.ts @@ -1,4 +1,4 @@ -import iso from "../../isomorph"; +import iso from "#braintrust-platform"; type ClaudeLocalToolParentResolver = (toolUseId: string) => Promise; diff --git a/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts index 98973ee5a..27f3a8642 100644 --- a/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts +++ b/js/src/instrumentation/providers/claude-agent-sdk-local-tool-spans.ts @@ -1,4 +1,7 @@ -import { _internalStartSpan as startBaseSpan, withCurrent } from "../../logger"; +import { + _internalStartSpan as startBaseSpan, + withCurrent, +} from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/cloudflare-agents-channels.ts b/js/src/instrumentation/providers/cloudflare-agents-channels.ts index 2f12705e1..db59327d7 100644 --- a/js/src/instrumentation/providers/cloudflare-agents-channels.ts +++ b/js/src/instrumentation/providers/cloudflare-agents-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -10,17 +14,28 @@ type CloudflareAgentsChannelContext = { self?: unknown; }; -export const cloudflareAgentsChannels = defineChannels( - "agents", - { - runAgentTool: channel< +export const cloudflareAgentsChannels: { + runAgentTool: MaterializedChannel< + ChannelSpec< [CloudflareAgentToolClass, CloudflareRunAgentToolOptions], CloudflareRunAgentToolResult, - CloudflareAgentsChannelContext - >({ - channelName: "Agent.runAgentTool", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.CLOUDFLARE_AGENTS }, -); + CloudflareAgentsChannelContext, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "agents", + { + runAgentTool: channel< + [CloudflareAgentToolClass, CloudflareRunAgentToolOptions], + CloudflareRunAgentToolResult, + CloudflareAgentsChannelContext + >({ + channelName: "Agent.runAgentTool", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.CLOUDFLARE_AGENTS }, + ))(); diff --git a/js/src/instrumentation/providers/cloudflare-agents-instrumentation.test.ts b/js/src/instrumentation/providers/cloudflare-agents-instrumentation.test.ts index 970297b01..abcf837f5 100644 --- a/js/src/instrumentation/providers/cloudflare-agents-instrumentation.test.ts +++ b/js/src/instrumentation/providers/cloudflare-agents-instrumentation.test.ts @@ -6,8 +6,8 @@ import { expectTypeOf, it, vi, -} from "vitest"; -import type { StartSpanArgs } from "../../logger"; +} from "vite-plus/test"; +import type { StartSpanArgs } from "../../logger/core"; import { getSpanInstrumentationName, INSTRUMENTATION_NAMES, @@ -17,18 +17,18 @@ const { mockStartSpan } = vi.hoisted(() => ({ mockStartSpan: vi.fn(), })); -vi.mock("../../logger", () => ({ +vi.mock("../../logger/core", () => ({ _internalStartSpanWithContext: (...args: unknown[]) => mockStartSpan(...args), })); -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { getEnv: vi.fn(), newTracingChannel: vi.fn(), }, })); -import iso from "../../isomorph"; +import iso from "#braintrust-platform"; import { registerCloudflareAgentsInstrumentation } from "./cloudflare-agents-instrumentation"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; diff --git a/js/src/instrumentation/providers/cloudflare-agents-instrumentation.ts b/js/src/instrumentation/providers/cloudflare-agents-instrumentation.ts index 36fe3671a..c05079b26 100644 --- a/js/src/instrumentation/providers/cloudflare-agents-instrumentation.ts +++ b/js/src/instrumentation/providers/cloudflare-agents-instrumentation.ts @@ -1,7 +1,7 @@ import { debugLogger } from "../../debug-logger"; -import type { IsoChannelHandlers } from "../../isomorph"; -import { _internalStartSpanWithContext } from "../../logger"; -import type { Span } from "../../logger"; +import type { IsoChannelHandlers } from "#braintrust-platform"; +import { _internalStartSpanWithContext } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/cloudflare-ai-chat-channels.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-channels.ts index c23871dbb..ace1e4f50 100644 --- a/js/src/instrumentation/providers/cloudflare-ai-chat-channels.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -10,26 +14,46 @@ type CloudflareAIChatChannelContext = { self?: unknown; }; -export const cloudflareAIChatChannels = defineChannels( - "@cloudflare/ai-chat", - { - runExclusiveChatTurn: channel< +export const cloudflareAIChatChannels: { + runExclusiveChatTurn: MaterializedChannel< + ChannelSpec< [string, CloudflareAIChatTurnCallback, CloudflareAIChatTurnOptions?], unknown, - CloudflareAIChatChannelContext - >({ - channelName: "AIChatAgent._runExclusiveChatTurn", - kind: "async", - }), - - onChatResponse: channel< + CloudflareAIChatChannelContext, + never, + "async" + > + >; + onChatResponse: MaterializedChannel< + ChannelSpec< [CloudflareAIChatResponseResult], unknown, - CloudflareAIChatChannelContext - >({ - channelName: "AIChatAgent.onChatResponse", - kind: "sync-stream", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.CLOUDFLARE_AI_CHAT }, -); + CloudflareAIChatChannelContext, + never, + "sync-stream" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@cloudflare/ai-chat", + { + runExclusiveChatTurn: channel< + [string, CloudflareAIChatTurnCallback, CloudflareAIChatTurnOptions?], + unknown, + CloudflareAIChatChannelContext + >({ + channelName: "AIChatAgent._runExclusiveChatTurn", + kind: "async", + }), + + onChatResponse: channel< + [CloudflareAIChatResponseResult], + unknown, + CloudflareAIChatChannelContext + >({ + channelName: "AIChatAgent.onChatResponse", + kind: "sync-stream", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.CLOUDFLARE_AI_CHAT }, + ))(); diff --git a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts index f0a0c3a72..368ea2469 100644 --- a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; const { mockInternalGetGlobalState, mockStartSpan, mockWithCurrent } = vi.hoisted(() => ({ @@ -9,17 +16,17 @@ const { mockInternalGetGlobalState, mockStartSpan, mockWithCurrent } = ), })); -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn() }, })); -vi.mock("../../logger", () => ({ +vi.mock("../../logger/core", () => ({ _internalGetGlobalState: () => mockInternalGetGlobalState(), startSpan: (...args: unknown[]) => (mockStartSpan as any)(...args), withCurrent: (...args: unknown[]) => (mockWithCurrent as any)(...args), })); -import iso from "../../isomorph"; +import iso from "#braintrust-platform"; import { INSTRUMENTATION_NAMES, INTERNAL_SPAN_INSTRUMENTATION_NAME, diff --git a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts index 2258ea666..cace2c844 100644 --- a/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts +++ b/js/src/instrumentation/providers/cloudflare-ai-chat-consumer.ts @@ -1,11 +1,14 @@ import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; +import type { + IsoChannelHandlers, + IsoTracingChannel, +} from "#braintrust-platform"; import { _internalGetGlobalState, startSpan as startBaseSpan, withCurrent, -} from "../../logger"; -import type { Span } from "../../logger"; +} from "../../logger/core"; +import type { Span } from "../../logger/core"; import { debugLogger } from "../../debug-logger"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/cloudflare-think-channels.ts b/js/src/instrumentation/providers/cloudflare-think-channels.ts index e7b50113c..c666ac99d 100644 --- a/js/src/instrumentation/providers/cloudflare-think-channels.ts +++ b/js/src/instrumentation/providers/cloudflare-think-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -11,17 +15,28 @@ type CloudflareThinkChannelContext = { moduleVersion?: string; }; -export const cloudflareThinkChannels = defineChannels( - "@cloudflare/think", - { - runInferenceLoop: channel< +export const cloudflareThinkChannels: { + runInferenceLoop: MaterializedChannel< + ChannelSpec< [CloudflareThinkTurnInput], CloudflareThinkStreamableResult, - CloudflareThinkChannelContext - >({ - channelName: "Think.runInferenceLoop", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.CLOUDFLARE_THINK }, -); + CloudflareThinkChannelContext, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@cloudflare/think", + { + runInferenceLoop: channel< + [CloudflareThinkTurnInput], + CloudflareThinkStreamableResult, + CloudflareThinkChannelContext + >({ + channelName: "Think.runInferenceLoop", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.CLOUDFLARE_THINK }, + ))(); diff --git a/js/src/instrumentation/providers/cloudflare-think-context.test.ts b/js/src/instrumentation/providers/cloudflare-think-context.test.ts new file mode 100644 index 000000000..cecaab977 --- /dev/null +++ b/js/src/instrumentation/providers/cloudflare-think-context.test.ts @@ -0,0 +1,21 @@ +import { expect, test, vi } from "vite-plus/test"; +import { _internalGetGlobalState, NoopSpan } from "../../logger/core"; + +test("shares active Think spans between separately loaded provider consumers", async () => { + const first = await import("./cloudflare-think-context"); + vi.resetModules(); + const second = await import("./cloudflare-think-context"); + const span = new NoopSpan(); + span.spanId = "think-cross-bundle-test"; + + first.registerCloudflareThinkSpan(span); + try { + _internalGetGlobalState().contextManager.runInContext(span, () => { + expect(second.currentCloudflareThinkSpan()).toBe(span); + }); + second.unregisterCloudflareThinkSpan(span); + expect(first.cloudflareThinkSpanCountForTesting()).toBe(0); + } finally { + first.unregisterCloudflareThinkSpan(span); + } +}); diff --git a/js/src/instrumentation/providers/cloudflare-think-context.ts b/js/src/instrumentation/providers/cloudflare-think-context.ts index 74c9fc6dd..8b2ebbdd1 100644 --- a/js/src/instrumentation/providers/cloudflare-think-context.ts +++ b/js/src/instrumentation/providers/cloudflare-think-context.ts @@ -1,16 +1,25 @@ -import { _internalGetGlobalState, type Span } from "../../logger"; +import { _internalGetGlobalState, type Span } from "../../logger/core"; -const cloudflareThinkSpans = new Map(); +const THINK_SPANS = Symbol.for("braintrust.cloudflare-think.spans.v1"); + +// The AI SDK and Think consumers may be registered by different bundled copies. +// Keep their active spans together, just like the global instrumentation registry. +function getThinkSpans(): Map { + const root = globalThis as typeof globalThis & { + [THINK_SPANS]?: Map; + }; + return (root[THINK_SPANS] ??= new Map()); +} export function registerCloudflareThinkSpan(span: Span): void { if (span.spanId) { - cloudflareThinkSpans.set(span.spanId, span); + getThinkSpans().set(span.spanId, span); } } export function unregisterCloudflareThinkSpan(span: Span): void { if (span.spanId) { - cloudflareThinkSpans.delete(span.spanId); + getThinkSpans().delete(span.spanId); } } @@ -19,7 +28,7 @@ export function currentCloudflareThinkSpan(): Span | undefined { _internalGetGlobalState()?.contextManager.getParentSpanIds()?.spanParents ?? []; for (const parentSpanId of parentSpanIds) { - const span = cloudflareThinkSpans.get(parentSpanId); + const span = getThinkSpans().get(parentSpanId); if (span) { return span; } @@ -28,5 +37,5 @@ export function currentCloudflareThinkSpan(): Span | undefined { } export function cloudflareThinkSpanCountForTesting(): number { - return cloudflareThinkSpans.size; + return getThinkSpans().size; } diff --git a/js/src/instrumentation/providers/cloudflare-think-instrumentation.test.ts b/js/src/instrumentation/providers/cloudflare-think-instrumentation.test.ts index f3e27919e..8654df230 100644 --- a/js/src/instrumentation/providers/cloudflare-think-instrumentation.test.ts +++ b/js/src/instrumentation/providers/cloudflare-think-instrumentation.test.ts @@ -1,12 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; import * as ai from "ai"; import { configureNode } from "../../node/config"; -import { - _exportsForTestingOnly, - initLogger, - type TestBackgroundLogger, -} from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger, type TestBackgroundLogger } from "../../logger/core"; import { wrapAISDK } from "../../wrappers/ai-sdk"; import { wrapCloudflareThink } from "../../wrappers/cloudflare-think"; import { cloudflareThinkSpanCountForTesting } from "./cloudflare-think-context"; diff --git a/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts index d19a5f95b..85ccaebec 100644 --- a/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts +++ b/js/src/instrumentation/providers/cloudflare-think-instrumentation.ts @@ -1,7 +1,7 @@ import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers } from "../../isomorph"; -import { _internalGetGlobalState, startSpan } from "../../logger"; -import type { Span } from "../../logger"; +import type { IsoChannelHandlers } from "#braintrust-platform"; +import { _internalGetGlobalState, startSpan } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/cohere-channels.ts b/js/src/instrumentation/providers/cohere-channels.ts index 54a145f5b..c048261ac 100644 --- a/js/src/instrumentation/providers/cohere-channels.ts +++ b/js/src/instrumentation/providers/cohere-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -11,33 +15,71 @@ import type { CohereRerankResponse, } from "../../vendor-sdk-types/cohere"; -export const cohereChannels = defineChannels( - "cohere-ai", - { - chat: channel<[CohereChatRequest], CohereChatResponse>({ - channelName: "chat", - kind: "async", - }), - - chatStream: channel< +export const cohereChannels: { + chat: MaterializedChannel< + ChannelSpec< + [CohereChatRequest], + CohereChatResponse, + Record, + never, + "async" + > + >; + chatStream: MaterializedChannel< + ChannelSpec< [CohereChatRequest], CohereChatStreamResult, Record, - CohereChatStreamEvent - >({ - channelName: "chatStream", - kind: "async", - }), + CohereChatStreamEvent, + "async" + > + >; + embed: MaterializedChannel< + ChannelSpec< + [CohereEmbedRequest], + CohereEmbedResponse, + Record, + never, + "async" + > + >; + rerank: MaterializedChannel< + ChannelSpec< + [CohereRerankRequest], + CohereRerankResponse, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "cohere-ai", + { + chat: channel<[CohereChatRequest], CohereChatResponse>({ + channelName: "chat", + kind: "async", + }), + + chatStream: channel< + [CohereChatRequest], + CohereChatStreamResult, + Record, + CohereChatStreamEvent + >({ + channelName: "chatStream", + kind: "async", + }), - embed: channel<[CohereEmbedRequest], CohereEmbedResponse>({ - channelName: "embed", - kind: "async", - }), + embed: channel<[CohereEmbedRequest], CohereEmbedResponse>({ + channelName: "embed", + kind: "async", + }), - rerank: channel<[CohereRerankRequest], CohereRerankResponse>({ - channelName: "rerank", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.COHERE }, -); + rerank: channel<[CohereRerankRequest], CohereRerankResponse>({ + channelName: "rerank", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.COHERE }, + ))(); diff --git a/js/src/instrumentation/providers/cohere-instrumentation.test.ts b/js/src/instrumentation/providers/cohere-instrumentation.test.ts index d7bd3603c..e36fbd518 100644 --- a/js/src/instrumentation/providers/cohere-instrumentation.test.ts +++ b/js/src/instrumentation/providers/cohere-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { aggregateCohereChatStreamChunks, extractCohereResponseMetadata, diff --git a/js/src/instrumentation/providers/cursor-sdk-channels.ts b/js/src/instrumentation/providers/cursor-sdk-channels.ts index 9ee604a1a..9eb3d523d 100644 --- a/js/src/instrumentation/providers/cursor-sdk-channels.ts +++ b/js/src/instrumentation/providers/cursor-sdk-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -9,44 +13,85 @@ import type { CursorSDKUserMessage, } from "../../vendor-sdk-types/cursor-sdk"; -export const cursorSDKChannels = defineChannels( - "@cursor/sdk", - { - create: channel< +export const cursorSDKChannels: { + create: MaterializedChannel< + ChannelSpec< [CursorSDKAgentOptions], CursorSDKAgent, - Record - >({ - channelName: "Agent.create", - kind: "async", - }), - resume: channel< + Record, + never, + "async" + > + >; + resume: MaterializedChannel< + ChannelSpec< [string, Partial | undefined], CursorSDKAgent, - Record - >({ - channelName: "Agent.resume", - kind: "async", - }), - prompt: channel< + Record, + never, + "async" + > + >; + prompt: MaterializedChannel< + ChannelSpec< [string | CursorSDKUserMessage, CursorSDKAgentOptions | undefined], CursorSDKRunResult, - Record - >({ - channelName: "Agent.prompt", - kind: "async", - }), - send: channel< + Record, + never, + "async" + > + >; + send: MaterializedChannel< + ChannelSpec< [string | CursorSDKUserMessage, CursorSDKSendOptions | undefined], CursorSDKRun, { agent?: CursorSDKAgent; operation?: "send"; - } - >({ - channelName: "agent.send", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.CURSOR_SDK }, -); + }, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@cursor/sdk", + { + create: channel< + [CursorSDKAgentOptions], + CursorSDKAgent, + Record + >({ + channelName: "Agent.create", + kind: "async", + }), + resume: channel< + [string, Partial | undefined], + CursorSDKAgent, + Record + >({ + channelName: "Agent.resume", + kind: "async", + }), + prompt: channel< + [string | CursorSDKUserMessage, CursorSDKAgentOptions | undefined], + CursorSDKRunResult, + Record + >({ + channelName: "Agent.prompt", + kind: "async", + }), + send: channel< + [string | CursorSDKUserMessage, CursorSDKSendOptions | undefined], + CursorSDKRun, + { + agent?: CursorSDKAgent; + operation?: "send"; + } + >({ + channelName: "agent.send", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.CURSOR_SDK }, + ))(); diff --git a/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts index 39d3582a6..f789421ab 100644 --- a/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/cursor-sdk-instrumentation.test.ts @@ -1,21 +1,28 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; const { mockStartSpan } = vi.hoisted(() => ({ mockStartSpan: vi.fn(), })); -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(), }, })); -vi.mock("../../logger", () => ({ +vi.mock("../../logger/core", () => ({ startSpan: (...args: unknown[]) => mockStartSpan(...args), _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), })); -import iso from "../../isomorph"; +import iso from "#braintrust-platform"; import { registerCursorSDKInstrumentation } from "./cursor-sdk-instrumentation"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; diff --git a/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts b/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts index 957d8a997..d374197ed 100644 --- a/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/cursor-sdk-instrumentation.ts @@ -1,9 +1,9 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers } from "../../isomorph"; +import type { IsoChannelHandlers } from "#braintrust-platform"; import { debugLogger } from "../../debug-logger"; -import { _internalStartSpan as startBaseSpan } from "../../logger"; -import type { Span } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/elevenlabs-channels.ts b/js/src/instrumentation/providers/elevenlabs-channels.ts index e5ba16be4..81a32229f 100644 --- a/js/src/instrumentation/providers/elevenlabs-channels.ts +++ b/js/src/instrumentation/providers/elevenlabs-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -8,29 +12,76 @@ import type { ElevenLabsTranscriptionRequest, } from "../../vendor-sdk-types/elevenlabs"; -export const elevenLabsChannels = defineChannels( - "@elevenlabs/elevenlabs-js", - { - convert: channel({ - channelName: "textToSpeech.convert", - kind: "async", - }), - stream: channel({ - channelName: "textToSpeech.stream", - kind: "async", - }), - convertWithTimestamps: channel< +export const elevenLabsChannels: { + convert: MaterializedChannel< + ChannelSpec< + ElevenLabsSpeechArgs, + ElevenLabsAudio, + Record, + never, + "async" + > + >; + stream: MaterializedChannel< + ChannelSpec< + ElevenLabsSpeechArgs, + ElevenLabsAudio, + Record, + never, + "async" + > + >; + convertWithTimestamps: MaterializedChannel< + ChannelSpec< ElevenLabsSpeechArgs, - ElevenLabsTimestampAudio - >({ channelName: "textToSpeech.convertWithTimestamps", kind: "async" }), - streamWithTimestamps: channel< + ElevenLabsTimestampAudio, + Record, + never, + "async" + > + >; + streamWithTimestamps: MaterializedChannel< + ChannelSpec< ElevenLabsSpeechArgs, - AsyncIterable - >({ channelName: "textToSpeech.streamWithTimestamps", kind: "async" }), - transcribe: channel< + AsyncIterable, + Record, + never, + "async" + > + >; + transcribe: MaterializedChannel< + ChannelSpec< [ElevenLabsTranscriptionRequest, unknown?], - ElevenLabsTranscription - >({ channelName: "speechToText.convert", kind: "async" }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.ELEVENLABS }, -); + ElevenLabsTranscription, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@elevenlabs/elevenlabs-js", + { + convert: channel({ + channelName: "textToSpeech.convert", + kind: "async", + }), + stream: channel({ + channelName: "textToSpeech.stream", + kind: "async", + }), + convertWithTimestamps: channel< + ElevenLabsSpeechArgs, + ElevenLabsTimestampAudio + >({ channelName: "textToSpeech.convertWithTimestamps", kind: "async" }), + streamWithTimestamps: channel< + ElevenLabsSpeechArgs, + AsyncIterable + >({ channelName: "textToSpeech.streamWithTimestamps", kind: "async" }), + transcribe: channel< + [ElevenLabsTranscriptionRequest, unknown?], + ElevenLabsTranscription + >({ channelName: "speechToText.convert", kind: "async" }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.ELEVENLABS }, + ))(); diff --git a/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts b/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts index 1e525ab09..b156cb297 100644 --- a/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts +++ b/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts @@ -1,16 +1,26 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { registerInstrumentation } from "../registrations/elevenlabs"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; import { Readable } from "node:stream"; import { ReadableStream, WritableStream, TransformStream, } from "node:stream/web"; -import { Attachment, _exportsForTestingOnly, initLogger } from "../../logger"; +import { Attachment, initLogger } from "../../logger/core"; +import { _exportsForTestingOnly } from "../../logger/testing"; import { configureNode } from "../../node/config"; import { elevenLabsChannels } from "./elevenlabs-channels"; import { wrapElevenLabs } from "../../wrappers/elevenlabs"; configureNode(); +registerInstrumentation(); const request = { text: "Hello", modelId: "eleven_flash_v2_5" }; describe("ElevenLabs instrumentation", () => { diff --git a/js/src/instrumentation/providers/elevenlabs-instrumentation.ts b/js/src/instrumentation/providers/elevenlabs-instrumentation.ts index d6a5a2941..927e1ca0b 100644 --- a/js/src/instrumentation/providers/elevenlabs-instrumentation.ts +++ b/js/src/instrumentation/providers/elevenlabs-instrumentation.ts @@ -6,7 +6,7 @@ import { startSpan, withCurrent, type Span, -} from "../../logger"; +} from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/eve-instrumentation.test.ts b/js/src/instrumentation/providers/eve-instrumentation.test.ts index 3a2b743a4..682c961af 100644 --- a/js/src/instrumentation/providers/eve-instrumentation.test.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.test.ts @@ -6,10 +6,11 @@ import { expect, it, vi, -} from "vitest"; +} from "vite-plus/test"; import * as braintrustExports from "../../exports"; import { configureNode } from "../../node/config"; -import { Attachment, _exportsForTestingOnly, initLogger } from "../../logger"; +import { Attachment, initLogger } from "../../logger/core"; +import { _exportsForTestingOnly } from "../../logger/testing"; import * as instrumentationExports from "../index"; import { braintrustEveInstrumentation } from "./eve-instrumentation"; import type { diff --git a/js/src/instrumentation/providers/eve-instrumentation.ts b/js/src/instrumentation/providers/eve-instrumentation.ts index 1ac8af24c..196c0bc91 100644 --- a/js/src/instrumentation/providers/eve-instrumentation.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.ts @@ -10,8 +10,8 @@ import { flush, updateSpan, withCurrent, -} from "../../logger"; -import type { Span } from "../../logger"; +} from "../../logger/core"; +import type { Span } from "../../logger/core"; import { LRUCache } from "../../lru-cache"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/flue-instrumentation.test.ts b/js/src/instrumentation/providers/flue-instrumentation.test.ts index d523de466..5f735a2cf 100644 --- a/js/src/instrumentation/providers/flue-instrumentation.test.ts +++ b/js/src/instrumentation/providers/flue-instrumentation.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; const { mockDebugLog, @@ -34,7 +41,7 @@ vi.mock("../../debug-logger", () => ({ }, })); -vi.mock("../../logger", () => ({ +vi.mock("../../logger/core", () => ({ NOOP_SPAN: {}, flush: (...args: unknown[]) => mockFlush(...args), _internalGetGlobalState: () => ({ diff --git a/js/src/instrumentation/providers/flue-instrumentation.ts b/js/src/instrumentation/providers/flue-instrumentation.ts index c83e54750..acecd38fc 100644 --- a/js/src/instrumentation/providers/flue-instrumentation.ts +++ b/js/src/instrumentation/providers/flue-instrumentation.ts @@ -6,8 +6,8 @@ import { _internalGetGlobalState, _internalStartSpan as startBaseSpan, withCurrent, -} from "../../logger"; -import type { Span, StartSpanArgs } from "../../logger"; +} from "../../logger/core"; +import type { Span, StartSpanArgs } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/genkit-channels.ts b/js/src/instrumentation/providers/genkit-channels.ts index 5f1ac13ff..bbd149a73 100644 --- a/js/src/instrumentation/providers/genkit-channels.ts +++ b/js/src/instrumentation/providers/genkit-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -11,59 +15,126 @@ import type { GenkitGenerateStreamResponse, } from "../../vendor-sdk-types/genkit"; -export const genkitChannels = defineChannels( - "@genkit-ai/ai", - { - generate: channel<[GenkitGenerateInput], GenkitGenerateResponse>({ - channelName: "generate", - kind: "async", - }), - - generateStream: channel< +export const genkitChannels: { + generate: MaterializedChannel< + ChannelSpec< + [GenkitGenerateInput], + GenkitGenerateResponse, + Record, + never, + "async" + > + >; + generateStream: MaterializedChannel< + ChannelSpec< [GenkitGenerateInput], GenkitGenerateStreamResponse, Record, - GenkitGenerateResponseChunk - >({ - channelName: "generateStream", - kind: "sync-stream", - }), + GenkitGenerateResponseChunk, + "sync-stream" + > + >; + embed: MaterializedChannel< + ChannelSpec< + [GenkitEmbedParams], + GenkitEmbedding[], + Record, + never, + "async" + > + >; + embedMany: MaterializedChannel< + ChannelSpec< + [GenkitEmbedManyParams], + unknown, + Record, + never, + "async" + > + >; + actionRun: MaterializedChannel< + ChannelSpec< + [unknown, unknown?], + unknown, + Record, + never, + "async" + > + >; + actionStream: MaterializedChannel< + ChannelSpec< + [unknown, unknown?], + ReturnType>, + Record, + unknown, + "sync-stream" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@genkit-ai/ai", + { + generate: channel<[GenkitGenerateInput], GenkitGenerateResponse>({ + channelName: "generate", + kind: "async", + }), - embed: channel<[GenkitEmbedParams], GenkitEmbedding[]>({ - channelName: "embed", - kind: "async", - }), + generateStream: channel< + [GenkitGenerateInput], + GenkitGenerateStreamResponse, + Record, + GenkitGenerateResponseChunk + >({ + channelName: "generateStream", + kind: "sync-stream", + }), - embedMany: channel<[GenkitEmbedManyParams], unknown>({ - channelName: "embedMany", - kind: "async", - }), + embed: channel<[GenkitEmbedParams], GenkitEmbedding[]>({ + channelName: "embed", + kind: "async", + }), - actionRun: channel<[unknown, unknown?], unknown>({ - channelName: "action.run", - kind: "async", - }), + embedMany: channel<[GenkitEmbedManyParams], unknown>({ + channelName: "embedMany", + kind: "async", + }), - actionStream: channel< - [unknown, unknown?], - ReturnType>, - Record, - unknown - >({ - channelName: "action.stream", - kind: "sync-stream", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.GENKIT }, -); + actionRun: channel<[unknown, unknown?], unknown>({ + channelName: "action.run", + kind: "async", + }), -export const genkitCoreChannels = defineChannels( - "@genkit-ai/core", - { - actionSpan: channel<[unknown, unknown, unknown?], unknown>({ - channelName: "action.span", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.GENKIT }, -); + actionStream: channel< + [unknown, unknown?], + ReturnType>, + Record, + unknown + >({ + channelName: "action.stream", + kind: "sync-stream", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.GENKIT }, + ))(); + +export const genkitCoreChannels: { + actionSpan: MaterializedChannel< + ChannelSpec< + [unknown, unknown, unknown?], + unknown, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@genkit-ai/core", + { + actionSpan: channel<[unknown, unknown, unknown?], unknown>({ + channelName: "action.span", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.GENKIT }, + ))(); diff --git a/js/src/instrumentation/providers/genkit-instrumentation.test.ts b/js/src/instrumentation/providers/genkit-instrumentation.test.ts index 11f172e5b..9997ab21b 100644 --- a/js/src/instrumentation/providers/genkit-instrumentation.test.ts +++ b/js/src/instrumentation/providers/genkit-instrumentation.test.ts @@ -1,5 +1,13 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger } from "../../logger/core"; import { registerGenkitInstrumentation } from "./genkit-instrumentation"; import { genkitChannels } from "./genkit-channels"; diff --git a/js/src/instrumentation/providers/genkit-instrumentation.ts b/js/src/instrumentation/providers/genkit-instrumentation.ts index 16abe033c..43bfad51f 100644 --- a/js/src/instrumentation/providers/genkit-instrumentation.ts +++ b/js/src/instrumentation/providers/genkit-instrumentation.ts @@ -5,12 +5,15 @@ import { } from "../core/channel-tracing"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; +import type { + IsoChannelHandlers, + IsoTracingChannel, +} from "#braintrust-platform"; import { _internalGetGlobalState, startSpan as startBaseSpan, -} from "../../logger"; -import type { CurrentSpanStore, Span } from "../../logger"; +} from "../../logger/core"; +import type { CurrentSpanStore, Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/github-copilot-channels.ts b/js/src/instrumentation/providers/github-copilot-channels.ts index 94b5abde9..67d15f0ff 100644 --- a/js/src/instrumentation/providers/github-copilot-channels.ts +++ b/js/src/instrumentation/providers/github-copilot-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -8,27 +12,59 @@ import type { GitHubCopilotSessionConfig, } from "../../vendor-sdk-types/github-copilot"; -export const gitHubCopilotChannels = defineChannels( - "@github/copilot-sdk", - { - createSession: channel<[GitHubCopilotSessionConfig], GitHubCopilotSession>({ - channelName: "client.createSession", - kind: "async", - }), - resumeSession: channel< +export const gitHubCopilotChannels: { + createSession: MaterializedChannel< + ChannelSpec< + [GitHubCopilotSessionConfig], + GitHubCopilotSession, + Record, + never, + "async" + > + >; + resumeSession: MaterializedChannel< + ChannelSpec< [string, GitHubCopilotResumeSessionConfig], - GitHubCopilotSession - >({ - channelName: "client.resumeSession", - kind: "async", - }), - sendAndWait: channel< + GitHubCopilotSession, + Record, + never, + "async" + > + >; + sendAndWait: MaterializedChannel< + ChannelSpec< [GitHubCopilotMessageOptions, number?], - GitHubCopilotAssistantMessageEvent | undefined - >({ - channelName: "session.sendAndWait", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.GITHUB_COPILOT }, -); + GitHubCopilotAssistantMessageEvent | undefined, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@github/copilot-sdk", + { + createSession: channel< + [GitHubCopilotSessionConfig], + GitHubCopilotSession + >({ + channelName: "client.createSession", + kind: "async", + }), + resumeSession: channel< + [string, GitHubCopilotResumeSessionConfig], + GitHubCopilotSession + >({ + channelName: "client.resumeSession", + kind: "async", + }), + sendAndWait: channel< + [GitHubCopilotMessageOptions, number?], + GitHubCopilotAssistantMessageEvent | undefined + >({ + channelName: "session.sendAndWait", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.GITHUB_COPILOT }, + ))(); diff --git a/js/src/instrumentation/providers/github-copilot-instrumentation.test.ts b/js/src/instrumentation/providers/github-copilot-instrumentation.test.ts index 5e88b3b51..5f81a9969 100644 --- a/js/src/instrumentation/providers/github-copilot-instrumentation.test.ts +++ b/js/src/instrumentation/providers/github-copilot-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { registerGitHubCopilotInstrumentation, extractMetricsFromUsage, diff --git a/js/src/instrumentation/providers/github-copilot-instrumentation.ts b/js/src/instrumentation/providers/github-copilot-instrumentation.ts index bf53ebd72..978e5377a 100644 --- a/js/src/instrumentation/providers/github-copilot-instrumentation.ts +++ b/js/src/instrumentation/providers/github-copilot-instrumentation.ts @@ -1,6 +1,6 @@ -import type { IsoChannelHandlers } from "../../isomorph"; -import { _internalStartSpan as startBaseSpan } from "../../logger"; -import type { Span } from "../../logger"; +import type { IsoChannelHandlers } from "#braintrust-platform"; +import { _internalStartSpan as startBaseSpan } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/google-adk-channels.ts b/js/src/instrumentation/providers/google-adk-channels.ts index 9d66b91b2..b00eae1ee 100644 --- a/js/src/instrumentation/providers/google-adk-channels.ts +++ b/js/src/instrumentation/providers/google-adk-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -20,37 +24,66 @@ type GoogleADKChannelContext = { * tool.runAsync is a regular async function returning Promise, * so it uses "async" kind. */ -export const googleADKChannels = defineChannels( - "@google/adk", - { - runnerRunAsync: channel< +export const googleADKChannels: { + runnerRunAsync: MaterializedChannel< + ChannelSpec< [GoogleADKRunAsyncParams], AsyncGenerator, GoogleADKChannelContext, - GoogleADKEvent - >({ - channelName: "runner.runAsync", - kind: "sync-stream", - }), - - agentRunAsync: channel< + GoogleADKEvent, + "sync-stream" + > + >; + agentRunAsync: MaterializedChannel< + ChannelSpec< [unknown], AsyncGenerator, GoogleADKChannelContext, - GoogleADKEvent - >({ - channelName: "agent.runAsync", - kind: "sync-stream", - }), - - toolRunAsync: channel< + GoogleADKEvent, + "sync-stream" + > + >; + toolRunAsync: MaterializedChannel< + ChannelSpec< [GoogleADKToolRunRequest], unknown, - GoogleADKChannelContext - >({ - channelName: "tool.runAsync", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.GOOGLE_ADK }, -); + GoogleADKChannelContext, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@google/adk", + { + runnerRunAsync: channel< + [GoogleADKRunAsyncParams], + AsyncGenerator, + GoogleADKChannelContext, + GoogleADKEvent + >({ + channelName: "runner.runAsync", + kind: "sync-stream", + }), + + agentRunAsync: channel< + [unknown], + AsyncGenerator, + GoogleADKChannelContext, + GoogleADKEvent + >({ + channelName: "agent.runAsync", + kind: "sync-stream", + }), + + toolRunAsync: channel< + [GoogleADKToolRunRequest], + unknown, + GoogleADKChannelContext + >({ + channelName: "tool.runAsync", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.GOOGLE_ADK }, + ))(); diff --git a/js/src/instrumentation/providers/google-adk-instrumentation.test.ts b/js/src/instrumentation/providers/google-adk-instrumentation.test.ts index 12c8a9175..dc21b916f 100644 --- a/js/src/instrumentation/providers/google-adk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-adk-instrumentation.test.ts @@ -1,18 +1,25 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + describe, + it, + expect, + vi, + beforeEach, + afterEach, +} from "vite-plus/test"; const { mockInternalGetGlobalState } = vi.hoisted(() => ({ mockInternalGetGlobalState: vi.fn(() => undefined), })); // Mock iso's newTracingChannel — must be before any imports that use it -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(), }, })); import { registerGoogleADKInstrumentation } from "./google-adk-instrumentation"; -import iso from "../../isomorph"; +import iso from "#braintrust-platform"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; @@ -23,7 +30,7 @@ const mockStartSpan = vi.fn(() => ({ export: vi.fn(() => Promise.resolve("mock-span-export")), })); -vi.mock("../../logger", () => ({ +vi.mock("../../logger/core", () => ({ startSpan: (...args: any[]) => (mockStartSpan as any)(...args), _internalGetGlobalState: (...args: any[]) => (mockInternalGetGlobalState as any)(...args), diff --git a/js/src/instrumentation/providers/google-adk-instrumentation.ts b/js/src/instrumentation/providers/google-adk-instrumentation.ts index de631de88..cea8a1105 100644 --- a/js/src/instrumentation/providers/google-adk-instrumentation.ts +++ b/js/src/instrumentation/providers/google-adk-instrumentation.ts @@ -1,12 +1,15 @@ import type { ChannelMessage } from "../core/channel-definitions"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; -import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; +import type { + IsoChannelHandlers, + IsoTracingChannel, +} from "#braintrust-platform"; import { _internalGetGlobalState, startSpan as startBaseSpan, withCurrent, -} from "../../logger"; -import type { CurrentSpanStore, Span } from "../../logger"; +} from "../../logger/core"; +import type { CurrentSpanStore, Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/google-genai-channels.ts b/js/src/instrumentation/providers/google-genai-channels.ts index 2003f472f..1d8c81bb7 100644 --- a/js/src/instrumentation/providers/google-genai-channels.ts +++ b/js/src/instrumentation/providers/google-genai-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -23,66 +27,140 @@ type GoogleGenAIInteractionResult = | GoogleGenAIInteraction | AsyncIterable; -export const googleGenAIChannels = defineChannels( - "@google/genai", - { - generateContent: channel< +export const googleGenAIChannels: { + generateContent: MaterializedChannel< + ChannelSpec< [GoogleGenAIGenerateContentParams], - GoogleGenAIGenerateContentResponse - >({ - channelName: "models.generateContent", - kind: "async", - }), - generateContentStream: channel< + GoogleGenAIGenerateContentResponse, + Record, + never, + "async" + > + >; + generateContentStream: MaterializedChannel< + ChannelSpec< [GoogleGenAIGenerateContentParams], GoogleGenAIStreamingResult, Record, - GoogleGenAIGenerateContentResponse - >({ - channelName: "models.generateContentStream", - kind: "async", - }), - embedContent: channel< + GoogleGenAIGenerateContentResponse, + "async" + > + >; + embedContent: MaterializedChannel< + ChannelSpec< [GoogleGenAIEmbedContentParams], - GoogleGenAIEmbedContentResponse - >({ - channelName: "models.embedContent", - kind: "async", - }), - generateImages: channel< + GoogleGenAIEmbedContentResponse, + Record, + never, + "async" + > + >; + generateImages: MaterializedChannel< + ChannelSpec< [GoogleGenAIGenerateImagesParams], - GoogleGenAIGenerateImagesResponse - >({ - channelName: "models.generateImages", - kind: "async", - }), - editImage: channel< + GoogleGenAIGenerateImagesResponse, + Record, + never, + "async" + > + >; + editImage: MaterializedChannel< + ChannelSpec< [GoogleGenAIEditImageParams], - GoogleGenAIEditImageResponse - >({ - channelName: "models.editImage", - kind: "async", - }), - generateVideos: channel< + GoogleGenAIEditImageResponse, + Record, + never, + "async" + > + >; + generateVideos: MaterializedChannel< + ChannelSpec< [GoogleGenAIGenerateVideosParams], - GoogleGenAIGenerateVideosOperation - >({ - channelName: "models.generateVideos", - kind: "async", - }), - httpResponseJson: channel<[], GoogleGenAIEmbedContentResponse>({ - channelName: "httpResponse.json", - kind: "async", - }), - interactionsCreate: channel< + GoogleGenAIGenerateVideosOperation, + Record, + never, + "async" + > + >; + httpResponseJson: MaterializedChannel< + ChannelSpec< + [], + GoogleGenAIEmbedContentResponse, + Record, + never, + "async" + > + >; + interactionsCreate: MaterializedChannel< + ChannelSpec< [GoogleGenAIInteractionCreateParams, Record?], GoogleGenAIInteractionResult, Record, - GoogleGenAIInteractionSSEEvent - >({ - channelName: "interactions.create", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.GOOGLE_GENAI }, -); + GoogleGenAIInteractionSSEEvent, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@google/genai", + { + generateContent: channel< + [GoogleGenAIGenerateContentParams], + GoogleGenAIGenerateContentResponse + >({ + channelName: "models.generateContent", + kind: "async", + }), + generateContentStream: channel< + [GoogleGenAIGenerateContentParams], + GoogleGenAIStreamingResult, + Record, + GoogleGenAIGenerateContentResponse + >({ + channelName: "models.generateContentStream", + kind: "async", + }), + embedContent: channel< + [GoogleGenAIEmbedContentParams], + GoogleGenAIEmbedContentResponse + >({ + channelName: "models.embedContent", + kind: "async", + }), + generateImages: channel< + [GoogleGenAIGenerateImagesParams], + GoogleGenAIGenerateImagesResponse + >({ + channelName: "models.generateImages", + kind: "async", + }), + editImage: channel< + [GoogleGenAIEditImageParams], + GoogleGenAIEditImageResponse + >({ + channelName: "models.editImage", + kind: "async", + }), + generateVideos: channel< + [GoogleGenAIGenerateVideosParams], + GoogleGenAIGenerateVideosOperation + >({ + channelName: "models.generateVideos", + kind: "async", + }), + httpResponseJson: channel<[], GoogleGenAIEmbedContentResponse>({ + channelName: "httpResponse.json", + kind: "async", + }), + interactionsCreate: channel< + [GoogleGenAIInteractionCreateParams, Record?], + GoogleGenAIInteractionResult, + Record, + GoogleGenAIInteractionSSEEvent + >({ + channelName: "interactions.create", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.GOOGLE_GENAI }, + ))(); diff --git a/js/src/instrumentation/providers/google-genai-embedding-usage.test.ts b/js/src/instrumentation/providers/google-genai-embedding-usage.test.ts index 35a1b726b..b491d143b 100644 --- a/js/src/instrumentation/providers/google-genai-embedding-usage.test.ts +++ b/js/src/instrumentation/providers/google-genai-embedding-usage.test.ts @@ -1,5 +1,13 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { _exportsForTestingOnly, initLogger, traced } from "../../logger"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger, traced } from "../../logger/core"; import { configureNode } from "../../node/config"; import { wrapGoogleGenAI } from "../../wrappers/google-genai"; import type { GoogleGenAIEmbedContentResponse } from "../../vendor-sdk-types/google-genai"; diff --git a/js/src/instrumentation/providers/google-genai-instrumentation.test.ts b/js/src/instrumentation/providers/google-genai-instrumentation.test.ts index b495cbf65..fdc776961 100644 --- a/js/src/instrumentation/providers/google-genai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.test.ts @@ -1,7 +1,14 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + describe, + it, + expect, + vi, + beforeEach, + afterEach, +} from "vite-plus/test"; // Mock iso's newTracingChannel - must be before any imports that use it -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newAsyncLocalStorage: vi.fn(() => { let current: unknown; @@ -26,15 +33,15 @@ vi.mock("../../isomorph", () => ({ })); import { registerGoogleGenAIInstrumentation } from "./google-genai-instrumentation"; -import { startSpan } from "../../logger"; -import iso from "../../isomorph"; +import { startSpan } from "../../logger/core"; +import iso from "#braintrust-platform"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; const mockStartSpan = vi.mocked(startSpan); // Mock logger -vi.mock("../../logger", async () => { - const { default: iso } = await import("../../isomorph"); +vi.mock("../../logger/core", async () => { + const { default: iso } = await import("#braintrust-platform"); return { _internalCaptureAttachmentsEnabled: () => iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", diff --git a/js/src/instrumentation/providers/google-genai-instrumentation.ts b/js/src/instrumentation/providers/google-genai-instrumentation.ts index a0608b143..0b6bf29c4 100644 --- a/js/src/instrumentation/providers/google-genai-instrumentation.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.ts @@ -11,7 +11,10 @@ import type { ErrorOf, StartOf, } from "../core/channel-definitions"; -import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; +import type { + IsoChannelHandlers, + IsoTracingChannel, +} from "#braintrust-platform"; import { _internalCaptureAttachmentsEnabled, _internalGetGlobalState, @@ -23,7 +26,7 @@ import { type CurrentSpanStore, type Span, type StartSpanArgs, -} from "../../logger"; +} from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/google-generative-ai-channels.ts b/js/src/instrumentation/providers/google-generative-ai-channels.ts index 66197b7ab..d5c814509 100644 --- a/js/src/instrumentation/providers/google-generative-ai-channels.ts +++ b/js/src/instrumentation/providers/google-generative-ai-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -5,33 +9,92 @@ import type { GenerativeAIModel, } from "../../vendor-sdk-types/google-generative-ai"; -export const googleGenerativeAIChannels = defineChannels( - "@google/generative-ai", - { - generateContent: channel< +export const googleGenerativeAIChannels: { + generateContent: MaterializedChannel< + ChannelSpec< Parameters, - Awaited> - >({ channelName: "GenerativeModel.generateContent", kind: "async" }), - generateContentStream: channel< + Awaited>, + Record, + never, + "async" + > + >; + generateContentStream: MaterializedChannel< + ChannelSpec< Parameters, - Awaited> - >({ channelName: "GenerativeModel.generateContentStream", kind: "async" }), - embedContent: channel< + Awaited>, + Record, + never, + "async" + > + >; + embedContent: MaterializedChannel< + ChannelSpec< Parameters, - Awaited> - >({ channelName: "GenerativeModel.embedContent", kind: "async" }), - batchEmbedContents: channel< + Awaited>, + Record, + never, + "async" + > + >; + batchEmbedContents: MaterializedChannel< + ChannelSpec< Parameters, - Awaited> - >({ channelName: "GenerativeModel.batchEmbedContents", kind: "async" }), - sendMessage: channel< + Awaited>, + Record, + never, + "async" + > + >; + sendMessage: MaterializedChannel< + ChannelSpec< Parameters, - Awaited> - >({ channelName: "ChatSession.sendMessage", kind: "async" }), - sendMessageStream: channel< + Awaited>, + Record, + never, + "async" + > + >; + sendMessageStream: MaterializedChannel< + ChannelSpec< Parameters, - Awaited> - >({ channelName: "ChatSession.sendMessageStream", kind: "async" }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.GOOGLE_GENERATIVE_AI }, -); + Awaited>, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@google/generative-ai", + { + generateContent: channel< + Parameters, + Awaited> + >({ channelName: "GenerativeModel.generateContent", kind: "async" }), + generateContentStream: channel< + Parameters, + Awaited> + >({ + channelName: "GenerativeModel.generateContentStream", + kind: "async", + }), + embedContent: channel< + Parameters, + Awaited> + >({ channelName: "GenerativeModel.embedContent", kind: "async" }), + batchEmbedContents: channel< + Parameters, + Awaited> + >({ channelName: "GenerativeModel.batchEmbedContents", kind: "async" }), + sendMessage: channel< + Parameters, + Awaited> + >({ channelName: "ChatSession.sendMessage", kind: "async" }), + sendMessageStream: channel< + Parameters, + Awaited> + >({ channelName: "ChatSession.sendMessageStream", kind: "async" }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.GOOGLE_GENERATIVE_AI }, + ))(); diff --git a/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts b/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts index 4b669aca1..d9abb2e10 100644 --- a/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts @@ -6,8 +6,9 @@ import { expect, it, vi, -} from "vitest"; -import { Attachment, _exportsForTestingOnly, initLogger } from "../../logger"; +} from "vite-plus/test"; +import { Attachment, initLogger } from "../../logger/core"; +import { _exportsForTestingOnly } from "../../logger/testing"; import { configureNode } from "../../node/config"; import { wrapGoogleGenerativeAI } from "../../wrappers/google-generative-ai"; import { googleGenerativeAIChannels as channels } from "./google-generative-ai-channels"; diff --git a/js/src/instrumentation/providers/google-generative-ai-instrumentation.ts b/js/src/instrumentation/providers/google-generative-ai-instrumentation.ts index de1c6852b..1e46e02d6 100644 --- a/js/src/instrumentation/providers/google-generative-ai-instrumentation.ts +++ b/js/src/instrumentation/providers/google-generative-ai-instrumentation.ts @@ -1,7 +1,7 @@ -import { _internalCaptureAttachmentsEnabled } from "../../logger"; +import { _internalCaptureAttachmentsEnabled } from "../../logger/core"; import { SpanTypeAttribute } from "../../../util/index"; import { debugLogger } from "../../debug-logger"; -import { startSpan, withCurrent, type Span } from "../../logger"; +import { startSpan, withCurrent, type Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/groq-channels.ts b/js/src/instrumentation/providers/groq-channels.ts index 5506e459e..c250456ba 100644 --- a/js/src/instrumentation/providers/groq-channels.ts +++ b/js/src/instrumentation/providers/groq-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -15,50 +19,97 @@ import type { type GroqChatResult = GroqChatCompletion | GroqChatStream; -export const groqChannels = defineChannels( - "groq-sdk", - { - chatCompletionsCreate: channel< +export const groqChannels: { + chatCompletionsCreate: MaterializedChannel< + ChannelSpec< [GroqChatCreateParams, unknown?], GroqChatResult, Record, - GroqChatCompletionChunk - >({ - channelName: "chat.completions.create", - kind: "async", - }), - - embeddingsCreate: channel< + GroqChatCompletionChunk, + "async" + > + >; + embeddingsCreate: MaterializedChannel< + ChannelSpec< [GroqEmbeddingCreateParams, unknown?], - GroqEmbeddingResponse - >({ - channelName: "embeddings.create", - kind: "async", - }), - - audioSpeechCreate: channel< + GroqEmbeddingResponse, + Record, + never, + "async" + > + >; + audioSpeechCreate: MaterializedChannel< + ChannelSpec< [GroqAudioSpeechCreateParams, unknown?], - Response - >({ - channelName: "audio.speech.create", - kind: "async", - }), - - audioTranscriptionsCreate: channel< + Response, + Record, + never, + "async" + > + >; + audioTranscriptionsCreate: MaterializedChannel< + ChannelSpec< [GroqAudioTranscriptionCreateParams, unknown?], - GroqAudioTextResult | string - >({ - channelName: "audio.transcriptions.create", - kind: "async", - }), - - audioTranslationsCreate: channel< + GroqAudioTextResult | string, + Record, + never, + "async" + > + >; + audioTranslationsCreate: MaterializedChannel< + ChannelSpec< [GroqAudioTranslationCreateParams, unknown?], - GroqAudioTextResult | string - >({ - channelName: "audio.translations.create", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.GROQ }, -); + GroqAudioTextResult | string, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "groq-sdk", + { + chatCompletionsCreate: channel< + [GroqChatCreateParams, unknown?], + GroqChatResult, + Record, + GroqChatCompletionChunk + >({ + channelName: "chat.completions.create", + kind: "async", + }), + + embeddingsCreate: channel< + [GroqEmbeddingCreateParams, unknown?], + GroqEmbeddingResponse + >({ + channelName: "embeddings.create", + kind: "async", + }), + + audioSpeechCreate: channel< + [GroqAudioSpeechCreateParams, unknown?], + Response + >({ + channelName: "audio.speech.create", + kind: "async", + }), + + audioTranscriptionsCreate: channel< + [GroqAudioTranscriptionCreateParams, unknown?], + GroqAudioTextResult | string + >({ + channelName: "audio.transcriptions.create", + kind: "async", + }), + + audioTranslationsCreate: channel< + [GroqAudioTranslationCreateParams, unknown?], + GroqAudioTextResult | string + >({ + channelName: "audio.translations.create", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.GROQ }, + ))(); diff --git a/js/src/instrumentation/providers/groq-instrumentation.test.ts b/js/src/instrumentation/providers/groq-instrumentation.test.ts index d70b8796f..c76e1ffd8 100644 --- a/js/src/instrumentation/providers/groq-instrumentation.test.ts +++ b/js/src/instrumentation/providers/groq-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { aggregateGroqChatCompletionChunks, parseGroqMetrics, diff --git a/js/src/instrumentation/providers/groq-instrumentation.ts b/js/src/instrumentation/providers/groq-instrumentation.ts index 610076cfe..c5c378042 100644 --- a/js/src/instrumentation/providers/groq-instrumentation.ts +++ b/js/src/instrumentation/providers/groq-instrumentation.ts @@ -12,7 +12,7 @@ import { Attachment, withCurrent, type Span, -} from "../../logger"; +} from "../../logger/core"; import { convertDataToBlob, getExtensionFromMediaType, @@ -265,7 +265,7 @@ function observeGroqAudioInput( filename, byte_size: data.byteLength, file_data: new Attachment({ - data: data.buffer, + data: Uint8Array.from(data).buffer, filename, contentType, }), @@ -431,7 +431,7 @@ function captureGroqSpeechResponse( filename, byte_size: data.byteLength, file_data: new Attachment({ - data: data.buffer, + data: Uint8Array.from(data).buffer, filename, contentType, }), diff --git a/js/src/instrumentation/providers/harness-agent-context.test.ts b/js/src/instrumentation/providers/harness-agent-context.test.ts index de9ff7722..7da346c92 100644 --- a/js/src/instrumentation/providers/harness-agent-context.test.ts +++ b/js/src/instrumentation/providers/harness-agent-context.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, test, vi } from "vitest"; -import { _internalGetGlobalState, type Span } from "../../logger"; +import { describe, expect, test, vi } from "vite-plus/test"; +import { _internalGetGlobalState, type Span } from "../../logger/core"; import { configureNode } from "../../node/config"; import { SpanObjectTypeV3 } from "../../util"; import { SpanComponentsV4 } from "../../../util/span_identifier_v4"; diff --git a/js/src/instrumentation/providers/huggingface-channels.ts b/js/src/instrumentation/providers/huggingface-channels.ts index 6996a9cc3..614c0cbb3 100644 --- a/js/src/instrumentation/providers/huggingface-channels.ts +++ b/js/src/instrumentation/providers/huggingface-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -11,52 +15,99 @@ import type { HuggingFaceTextGenerationStreamOutput, } from "../../vendor-sdk-types/huggingface"; -export const huggingFaceChannels = defineChannels( - "@huggingface/inference", - { - chatCompletion: channel< +export const huggingFaceChannels: { + chatCompletion: MaterializedChannel< + ChannelSpec< [HuggingFaceChatCompletionParams], - HuggingFaceChatCompletion - >({ - channelName: "chatCompletion", - kind: "async", - }), - - chatCompletionStream: channel< + HuggingFaceChatCompletion, + Record, + never, + "async" + > + >; + chatCompletionStream: MaterializedChannel< + ChannelSpec< [HuggingFaceChatCompletionParams], AsyncIterable, Record, - HuggingFaceChatCompletionChunk - >({ - channelName: "chatCompletionStream", - kind: "sync-stream", - }), - - textGeneration: channel< + HuggingFaceChatCompletionChunk, + "sync-stream" + > + >; + textGeneration: MaterializedChannel< + ChannelSpec< [HuggingFaceTextGenerationParams], - HuggingFaceTextGenerationOutput - >({ - channelName: "textGeneration", - kind: "async", - }), - - textGenerationStream: channel< + HuggingFaceTextGenerationOutput, + Record, + never, + "async" + > + >; + textGenerationStream: MaterializedChannel< + ChannelSpec< [HuggingFaceTextGenerationParams], AsyncIterable, Record, - HuggingFaceTextGenerationStreamOutput - >({ - channelName: "textGenerationStream", - kind: "sync-stream", - }), - - featureExtraction: channel< + HuggingFaceTextGenerationStreamOutput, + "sync-stream" + > + >; + featureExtraction: MaterializedChannel< + ChannelSpec< [HuggingFaceFeatureExtractionParams], - HuggingFaceFeatureExtractionOutput - >({ - channelName: "featureExtraction", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.HUGGINGFACE }, -); + HuggingFaceFeatureExtractionOutput, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@huggingface/inference", + { + chatCompletion: channel< + [HuggingFaceChatCompletionParams], + HuggingFaceChatCompletion + >({ + channelName: "chatCompletion", + kind: "async", + }), + + chatCompletionStream: channel< + [HuggingFaceChatCompletionParams], + AsyncIterable, + Record, + HuggingFaceChatCompletionChunk + >({ + channelName: "chatCompletionStream", + kind: "sync-stream", + }), + + textGeneration: channel< + [HuggingFaceTextGenerationParams], + HuggingFaceTextGenerationOutput + >({ + channelName: "textGeneration", + kind: "async", + }), + + textGenerationStream: channel< + [HuggingFaceTextGenerationParams], + AsyncIterable, + Record, + HuggingFaceTextGenerationStreamOutput + >({ + channelName: "textGenerationStream", + kind: "sync-stream", + }), + + featureExtraction: channel< + [HuggingFaceFeatureExtractionParams], + HuggingFaceFeatureExtractionOutput + >({ + channelName: "featureExtraction", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.HUGGINGFACE }, + ))(); diff --git a/js/src/instrumentation/providers/huggingface-instrumentation.test.ts b/js/src/instrumentation/providers/huggingface-instrumentation.test.ts index 6c55e8bbe..eadc924a5 100644 --- a/js/src/instrumentation/providers/huggingface-instrumentation.test.ts +++ b/js/src/instrumentation/providers/huggingface-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { aggregateChatCompletionChunks, extractResponseMetadata, diff --git a/js/src/instrumentation/providers/huggingface-instrumentation.ts b/js/src/instrumentation/providers/huggingface-instrumentation.ts index 0407adffa..d6b5e4fb1 100644 --- a/js/src/instrumentation/providers/huggingface-instrumentation.ts +++ b/js/src/instrumentation/providers/huggingface-instrumentation.ts @@ -14,7 +14,7 @@ import type { HuggingFaceTextGenerationDetails, HuggingFaceTextGenerationStreamOutput, } from "../../vendor-sdk-types/huggingface"; -import type { Span } from "../../logger"; +import type { Span } from "../../logger/core"; const REQUEST_METADATA_ALLOWLIST = new Set([ "dimensions", diff --git a/js/src/instrumentation/providers/huggingface-transformers-channels.ts b/js/src/instrumentation/providers/huggingface-transformers-channels.ts index ac2f1fcbe..b46a42841 100644 --- a/js/src/instrumentation/providers/huggingface-transformers-channels.ts +++ b/js/src/instrumentation/providers/huggingface-transformers-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -54,26 +58,46 @@ export function getHuggingFaceTransformersPipelineInfo( return pipeline ? pipelineInfo.get(pipeline) : undefined; } -export const huggingFaceTransformersChannels = defineChannels( - "@huggingface/transformers", - { - pipeline: channel< +export const huggingFaceTransformersChannels: { + pipeline: MaterializedChannel< + ChannelSpec< [string, (string | null)?, Record?], HuggingFaceTransformersPipeline, - HuggingFaceTransformersEventContext - >({ - channelName: "pipeline", - kind: "async", - }), - - pipelineCall: channel< + HuggingFaceTransformersEventContext, + never, + "async" + > + >; + pipelineCall: MaterializedChannel< + ChannelSpec< [unknown, ...unknown[]], unknown | HuggingFaceTransformersTensor, - HuggingFaceTransformersEventContext - >({ - channelName: "pipeline.call", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.HUGGINGFACE }, -); + HuggingFaceTransformersEventContext, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@huggingface/transformers", + { + pipeline: channel< + [string, (string | null)?, Record?], + HuggingFaceTransformersPipeline, + HuggingFaceTransformersEventContext + >({ + channelName: "pipeline", + kind: "async", + }), + + pipelineCall: channel< + [unknown, ...unknown[]], + unknown | HuggingFaceTransformersTensor, + HuggingFaceTransformersEventContext + >({ + channelName: "pipeline.call", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.HUGGINGFACE }, + ))(); diff --git a/js/src/instrumentation/providers/huggingface-transformers-instrumentation.test.ts b/js/src/instrumentation/providers/huggingface-transformers-instrumentation.test.ts index b32e93579..ecc545c64 100644 --- a/js/src/instrumentation/providers/huggingface-transformers-instrumentation.test.ts +++ b/js/src/instrumentation/providers/huggingface-transformers-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { registerHuggingFaceTransformersPipeline, type HuggingFaceTransformersEventContext, diff --git a/js/src/instrumentation/providers/huggingface-transformers-instrumentation.ts b/js/src/instrumentation/providers/huggingface-transformers-instrumentation.ts index 09e817685..1bb004837 100644 --- a/js/src/instrumentation/providers/huggingface-transformers-instrumentation.ts +++ b/js/src/instrumentation/providers/huggingface-transformers-instrumentation.ts @@ -1,6 +1,9 @@ import { traceAsyncChannel } from "../core/channel-tracing"; import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; +import type { + IsoChannelHandlers, + IsoTracingChannel, +} from "#braintrust-platform"; import { SpanTypeAttribute, isObject } from "../../../util"; import type { HuggingFaceTransformersPipeline } from "../../vendor-sdk-types/huggingface-transformers"; import { @@ -307,7 +310,12 @@ function choicesFromGenerations(result: unknown): unknown { return choices.length > 0 ? choices : undefined; } -export const _exportsForTestingOnly = { +export const _exportsForTestingOnly: { + extractInput: typeof extractInput; + extractMetadata: typeof extractMetadata; + extractOutput: typeof extractOutput; + isSupportedTask: typeof isSupportedHuggingFaceTransformersTask; +} = { extractInput, extractMetadata, extractOutput, diff --git a/js/src/instrumentation/providers/instrumentation-names.test.ts b/js/src/instrumentation/providers/instrumentation-names.test.ts index 052ef5e4e..90110aece 100644 --- a/js/src/instrumentation/providers/instrumentation-names.test.ts +++ b/js/src/instrumentation/providers/instrumentation-names.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import { aiSDKChannels, harnessAgentChannels } from "./ai-sdk-channels"; diff --git a/js/src/instrumentation/providers/langchain-channels.ts b/js/src/instrumentation/providers/langchain-channels.ts index 8ab3f2356..8e5680978 100644 --- a/js/src/instrumentation/providers/langchain-channels.ts +++ b/js/src/instrumentation/providers/langchain-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -5,23 +9,43 @@ import type { LangChainCallbackManagerConfigureResult, } from "../../vendor-sdk-types/langchain"; -export const langChainChannels = defineChannels( - "@langchain/core", - { - configure: channel< +export const langChainChannels: { + configure: MaterializedChannel< + ChannelSpec< LangChainCallbackManagerConfigureArgs, - LangChainCallbackManagerConfigureResult - >({ - channelName: "CallbackManager.configure", - kind: "sync-stream", - }), - configureSync: channel< + LangChainCallbackManagerConfigureResult, + Record, + never, + "sync-stream" + > + >; + configureSync: MaterializedChannel< + ChannelSpec< LangChainCallbackManagerConfigureArgs, - LangChainCallbackManagerConfigureResult - >({ - channelName: "CallbackManager._configureSync", - kind: "sync-stream", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.LANGCHAIN }, -); + LangChainCallbackManagerConfigureResult, + Record, + never, + "sync-stream" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@langchain/core", + { + configure: channel< + LangChainCallbackManagerConfigureArgs, + LangChainCallbackManagerConfigureResult + >({ + channelName: "CallbackManager.configure", + kind: "sync-stream", + }), + configureSync: channel< + LangChainCallbackManagerConfigureArgs, + LangChainCallbackManagerConfigureResult + >({ + channelName: "CallbackManager._configureSync", + kind: "sync-stream", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.LANGCHAIN }, + ))(); diff --git a/js/src/instrumentation/providers/langchain-instrumentation.test.ts b/js/src/instrumentation/providers/langchain-instrumentation.test.ts index 570972a9d..f48a10200 100644 --- a/js/src/instrumentation/providers/langchain-instrumentation.test.ts +++ b/js/src/instrumentation/providers/langchain-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vite-plus/test"; import { registerLangChainInstrumentation } from "./langchain-instrumentation"; import { langChainChannels } from "./langchain-channels"; diff --git a/js/src/instrumentation/providers/langchain-instrumentation.ts b/js/src/instrumentation/providers/langchain-instrumentation.ts index 42a5f7fd5..ff3cb71de 100644 --- a/js/src/instrumentation/providers/langchain-instrumentation.ts +++ b/js/src/instrumentation/providers/langchain-instrumentation.ts @@ -1,5 +1,8 @@ import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; +import type { + IsoChannelHandlers, + IsoTracingChannel, +} from "#braintrust-platform"; import type { LangChainCallbackManager } from "../../vendor-sdk-types/langchain"; import { BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME, diff --git a/js/src/instrumentation/providers/langgraph-sdk-channels.ts b/js/src/instrumentation/providers/langgraph-sdk-channels.ts index 1329982a5..cabbdfa8f 100644 --- a/js/src/instrumentation/providers/langgraph-sdk-channels.ts +++ b/js/src/instrumentation/providers/langgraph-sdk-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { LangGraphRunArgs, @@ -5,17 +9,37 @@ import type { } from "../../vendor-sdk-types/langgraph-sdk"; import { channel, defineChannels } from "../core/channel-definitions"; -export const langGraphSDKChannels = defineChannels( - "@langchain/langgraph-sdk", - { - wait: channel({ - channelName: "runs.wait", - kind: "async", - }), - stream: channel>({ - channelName: "runs.stream", - kind: "sync-stream", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.LANGGRAPH_SDK }, -); +export const langGraphSDKChannels: { + wait: MaterializedChannel< + ChannelSpec< + LangGraphRunArgs, + unknown, + Record, + never, + "async" + > + >; + stream: MaterializedChannel< + ChannelSpec< + LangGraphRunArgs, + AsyncGenerator, + Record, + never, + "sync-stream" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@langchain/langgraph-sdk", + { + wait: channel({ + channelName: "runs.wait", + kind: "async", + }), + stream: channel>({ + channelName: "runs.stream", + kind: "sync-stream", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.LANGGRAPH_SDK }, + ))(); diff --git a/js/src/instrumentation/providers/langgraph-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/langgraph-sdk-instrumentation.test.ts index 50dcfc6db..fd46be280 100644 --- a/js/src/instrumentation/providers/langgraph-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/langgraph-sdk-instrumentation.test.ts @@ -1,10 +1,20 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { registerInstrumentation } from "../registrations/langgraphSDK"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger } from "../../logger/core"; import { configureNode } from "../../node/config"; import { wrapLangGraphSDK } from "../../wrappers/langgraph-sdk"; import { langGraphSDKChannels } from "./langgraph-sdk-channels"; configureNode(); +registerInstrumentation(); describe("LangGraph SDK instrumentation", () => { let backgroundLogger: ReturnType< diff --git a/js/src/instrumentation/providers/langgraph-sdk-instrumentation.ts b/js/src/instrumentation/providers/langgraph-sdk-instrumentation.ts index 37f9a6cc0..a3f1da3f6 100644 --- a/js/src/instrumentation/providers/langgraph-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/langgraph-sdk-instrumentation.ts @@ -1,6 +1,6 @@ import { isObject, SpanTypeAttribute } from "../../../util"; import { debugLogger } from "../../debug-logger"; -import { startSpan, withCurrent, type Span } from "../../logger"; +import { startSpan, withCurrent, type Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/langsmith-channels.ts b/js/src/instrumentation/providers/langsmith-channels.ts index ff49a7e8e..529ec51a7 100644 --- a/js/src/instrumentation/providers/langsmith-channels.ts +++ b/js/src/instrumentation/providers/langsmith-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -6,30 +10,59 @@ import type { LangSmithRun, } from "../../vendor-sdk-types/langsmith"; -export const langSmithChannels = defineChannels( - "langsmith", - { - createRun: channel< +export const langSmithChannels: { + createRun: MaterializedChannel< + ChannelSpec< [run: LangSmithRun, options?: unknown], - Awaited>> - >({ - channelName: "Client.createRun", - kind: "async", - }), - updateRun: channel< + Awaited>>, + Record, + never, + "async" + > + >; + updateRun: MaterializedChannel< + ChannelSpec< [runId: string, run: LangSmithRun, options?: unknown], - Awaited>> - >({ - channelName: "Client.updateRun", - kind: "async", - }), - batchIngestRuns: channel< + Awaited>>, + Record, + never, + "async" + > + >; + batchIngestRuns: MaterializedChannel< + ChannelSpec< [runs: LangSmithBatchIngestRuns, options?: unknown], - Awaited>> - >({ - channelName: "Client.batchIngestRuns", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.LANGSMITH }, -); + Awaited>>, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "langsmith", + { + createRun: channel< + [run: LangSmithRun, options?: unknown], + Awaited>> + >({ + channelName: "Client.createRun", + kind: "async", + }), + updateRun: channel< + [runId: string, run: LangSmithRun, options?: unknown], + Awaited>> + >({ + channelName: "Client.updateRun", + kind: "async", + }), + batchIngestRuns: channel< + [runs: LangSmithBatchIngestRuns, options?: unknown], + Awaited>> + >({ + channelName: "Client.batchIngestRuns", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.LANGSMITH }, + ))(); diff --git a/js/src/instrumentation/providers/langsmith-instrumentation.test.ts b/js/src/instrumentation/providers/langsmith-instrumentation.test.ts index 55a986a74..640435273 100644 --- a/js/src/instrumentation/providers/langsmith-instrumentation.test.ts +++ b/js/src/instrumentation/providers/langsmith-instrumentation.test.ts @@ -1,6 +1,14 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; import { configureNode } from "../../node/config"; -import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger } from "../../logger/core"; import { configureInstrumentation } from "../registry"; import { langSmithChannels } from "./langsmith-channels"; import { registerLangSmithInstrumentation } from "./langsmith-instrumentation"; diff --git a/js/src/instrumentation/providers/langsmith-instrumentation.ts b/js/src/instrumentation/providers/langsmith-instrumentation.ts index 1716b88cb..8257276fd 100644 --- a/js/src/instrumentation/providers/langsmith-instrumentation.ts +++ b/js/src/instrumentation/providers/langsmith-instrumentation.ts @@ -1,7 +1,7 @@ import { SpanTypeAttribute } from "../../../util/index"; import { debugLogger } from "../../debug-logger"; -import { startSpan as startBaseSpan } from "../../logger"; -import type { Span } from "../../logger"; +import { startSpan as startBaseSpan } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -20,7 +20,7 @@ type ActiveRun = { }; type LangSmithInstrumentationOptions = { - skipLangChainRuns?: boolean; + skipLangChainRuns?: boolean | (() => boolean); }; const MAX_COMPLETED_RUNS = 10_000; @@ -46,7 +46,7 @@ class LangSmithInstrumentationConsumer { private readonly completedRuns = new LRUCache({ max: MAX_COMPLETED_RUNS, }); - private readonly skipLangChainRuns: boolean; + private readonly skipLangChainRuns: boolean | (() => boolean); constructor(options: LangSmithInstrumentationOptions = {}) { this.skipLangChainRuns = options.skipLangChainRuns ?? true; @@ -261,7 +261,11 @@ class LangSmithInstrumentationConsumer { } private shouldSkipLangChainRun(run: LangSmithRun): boolean { - if (!this.skipLangChainRuns) { + const skip = + typeof this.skipLangChainRuns === "function" + ? this.skipLangChainRuns() + : this.skipLangChainRuns; + if (!skip) { return false; } const serialized = ownValue(run, "serialized"); diff --git a/js/src/instrumentation/providers/mistral-channels.ts b/js/src/instrumentation/providers/mistral-channels.ts index 613c542cd..8a8fec2cf 100644 --- a/js/src/instrumentation/providers/mistral-channels.ts +++ b/js/src/instrumentation/providers/mistral-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -21,102 +25,203 @@ import type { MistralModerationResponse, } from "../../vendor-sdk-types/mistral"; -export const mistralChannels = defineChannels( - "@mistralai/mistralai", - { - chatComplete: channel< +export const mistralChannels: { + chatComplete: MaterializedChannel< + ChannelSpec< [MistralChatCreateParams], - MistralChatCompletionResponse - >({ - channelName: "chat.complete", - kind: "async", - }), - - chatStream: channel< + MistralChatCompletionResponse, + Record, + never, + "async" + > + >; + chatStream: MaterializedChannel< + ChannelSpec< [MistralChatCreateParams], MistralChatResult, Record, - MistralChatCompletionEvent - >({ - channelName: "chat.stream", - kind: "async", - }), - - embeddingsCreate: channel< + MistralChatCompletionEvent, + "async" + > + >; + embeddingsCreate: MaterializedChannel< + ChannelSpec< [MistralEmbeddingCreateParams], - MistralEmbeddingResponse - >({ - channelName: "embeddings.create", - kind: "async", - }), - - classifiersModerate: channel< + MistralEmbeddingResponse, + Record, + never, + "async" + > + >; + classifiersModerate: MaterializedChannel< + ChannelSpec< [MistralClassificationCreateParams], - MistralModerationResponse - >({ - channelName: "classifiers.moderate", - kind: "async", - }), - - classifiersModerateChat: channel< + MistralModerationResponse, + Record, + never, + "async" + > + >; + classifiersModerateChat: MaterializedChannel< + ChannelSpec< [MistralChatClassificationCreateParams], - MistralModerationResponse - >({ - channelName: "classifiers.moderateChat", - kind: "async", - }), - - classifiersClassify: channel< + MistralModerationResponse, + Record, + never, + "async" + > + >; + classifiersClassify: MaterializedChannel< + ChannelSpec< [MistralClassificationCreateParams], - MistralClassificationResponse - >({ - channelName: "classifiers.classify", - kind: "async", - }), - - classifiersClassifyChat: channel< + MistralClassificationResponse, + Record, + never, + "async" + > + >; + classifiersClassifyChat: MaterializedChannel< + ChannelSpec< [MistralChatClassificationCreateParams], - MistralClassificationResponse - >({ - channelName: "classifiers.classifyChat", - kind: "async", - }), - - fimComplete: channel< + MistralClassificationResponse, + Record, + never, + "async" + > + >; + fimComplete: MaterializedChannel< + ChannelSpec< [MistralFimCreateParams], - MistralFimCompletionResponse - >({ - channelName: "fim.complete", - kind: "async", - }), - - fimStream: channel< + MistralFimCompletionResponse, + Record, + never, + "async" + > + >; + fimStream: MaterializedChannel< + ChannelSpec< [MistralFimCreateParams], MistralFimResult, Record, - MistralFimCompletionEvent - >({ - channelName: "fim.stream", - kind: "async", - }), - - agentsComplete: channel< + MistralFimCompletionEvent, + "async" + > + >; + agentsComplete: MaterializedChannel< + ChannelSpec< [MistralAgentsCreateParams], - MistralAgentsCompletionResponse - >({ - channelName: "agents.complete", - kind: "async", - }), - - agentsStream: channel< + MistralAgentsCompletionResponse, + Record, + never, + "async" + > + >; + agentsStream: MaterializedChannel< + ChannelSpec< [MistralAgentsCreateParams], MistralAgentsResult, Record, - MistralAgentsCompletionEvent - >({ - channelName: "agents.stream", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.MISTRAL }, -); + MistralAgentsCompletionEvent, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@mistralai/mistralai", + { + chatComplete: channel< + [MistralChatCreateParams], + MistralChatCompletionResponse + >({ + channelName: "chat.complete", + kind: "async", + }), + + chatStream: channel< + [MistralChatCreateParams], + MistralChatResult, + Record, + MistralChatCompletionEvent + >({ + channelName: "chat.stream", + kind: "async", + }), + + embeddingsCreate: channel< + [MistralEmbeddingCreateParams], + MistralEmbeddingResponse + >({ + channelName: "embeddings.create", + kind: "async", + }), + + classifiersModerate: channel< + [MistralClassificationCreateParams], + MistralModerationResponse + >({ + channelName: "classifiers.moderate", + kind: "async", + }), + + classifiersModerateChat: channel< + [MistralChatClassificationCreateParams], + MistralModerationResponse + >({ + channelName: "classifiers.moderateChat", + kind: "async", + }), + + classifiersClassify: channel< + [MistralClassificationCreateParams], + MistralClassificationResponse + >({ + channelName: "classifiers.classify", + kind: "async", + }), + + classifiersClassifyChat: channel< + [MistralChatClassificationCreateParams], + MistralClassificationResponse + >({ + channelName: "classifiers.classifyChat", + kind: "async", + }), + + fimComplete: channel< + [MistralFimCreateParams], + MistralFimCompletionResponse + >({ + channelName: "fim.complete", + kind: "async", + }), + + fimStream: channel< + [MistralFimCreateParams], + MistralFimResult, + Record, + MistralFimCompletionEvent + >({ + channelName: "fim.stream", + kind: "async", + }), + + agentsComplete: channel< + [MistralAgentsCreateParams], + MistralAgentsCompletionResponse + >({ + channelName: "agents.complete", + kind: "async", + }), + + agentsStream: channel< + [MistralAgentsCreateParams], + MistralAgentsResult, + Record, + MistralAgentsCompletionEvent + >({ + channelName: "agents.stream", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.MISTRAL }, + ))(); diff --git a/js/src/instrumentation/providers/mistral-instrumentation.test.ts b/js/src/instrumentation/providers/mistral-instrumentation.test.ts index adab7b87b..804d2db3d 100644 --- a/js/src/instrumentation/providers/mistral-instrumentation.test.ts +++ b/js/src/instrumentation/providers/mistral-instrumentation.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { aggregateMistralStreamChunks, extractMistralRequestMetadata, diff --git a/js/src/instrumentation/providers/ollama-channels.ts b/js/src/instrumentation/providers/ollama-channels.ts index c6e4f2044..22702113c 100644 --- a/js/src/instrumentation/providers/ollama-channels.ts +++ b/js/src/instrumentation/providers/ollama-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { OllamaChatRequest, @@ -11,31 +15,60 @@ import type { } from "../../vendor-sdk-types/ollama"; import { channel, defineChannels } from "../core/channel-definitions"; -export const ollamaChannels = defineChannels( - "ollama", - { - chat: channel< +export const ollamaChannels: { + chat: MaterializedChannel< + ChannelSpec< [OllamaChatRequest], OllamaChatResult, Record, - OllamaChatResponse - >({ - channelName: "chat", - kind: "async", - }), - generate: channel< + OllamaChatResponse, + "async" + > + >; + generate: MaterializedChannel< + ChannelSpec< [OllamaGenerateRequest], OllamaGenerateResult, Record, - OllamaGenerateResponse - >({ - channelName: "generate", - kind: "async", - }), - embed: channel<[OllamaEmbedRequest], OllamaEmbedResponse>({ - channelName: "embed", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.OLLAMA }, -); + OllamaGenerateResponse, + "async" + > + >; + embed: MaterializedChannel< + ChannelSpec< + [OllamaEmbedRequest], + OllamaEmbedResponse, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "ollama", + { + chat: channel< + [OllamaChatRequest], + OllamaChatResult, + Record, + OllamaChatResponse + >({ + channelName: "chat", + kind: "async", + }), + generate: channel< + [OllamaGenerateRequest], + OllamaGenerateResult, + Record, + OllamaGenerateResponse + >({ + channelName: "generate", + kind: "async", + }), + embed: channel<[OllamaEmbedRequest], OllamaEmbedResponse>({ + channelName: "embed", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.OLLAMA }, + ))(); diff --git a/js/src/instrumentation/providers/ollama-instrumentation.test.ts b/js/src/instrumentation/providers/ollama-instrumentation.test.ts index 74bad0b46..802c1127e 100644 --- a/js/src/instrumentation/providers/ollama-instrumentation.test.ts +++ b/js/src/instrumentation/providers/ollama-instrumentation.test.ts @@ -1,5 +1,12 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import iso from "../../isomorph"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; +import iso from "#braintrust-platform"; import { configureNode } from "../../node/config"; import { aggregateOllamaChatChunks, diff --git a/js/src/instrumentation/providers/ollama-instrumentation.ts b/js/src/instrumentation/providers/ollama-instrumentation.ts index 17d5c23c5..c9a53fb98 100644 --- a/js/src/instrumentation/providers/ollama-instrumentation.ts +++ b/js/src/instrumentation/providers/ollama-instrumentation.ts @@ -1,6 +1,9 @@ import { SpanTypeAttribute, isObject } from "../../../util/index"; -import iso from "../../isomorph"; -import { _internalCaptureAttachmentsEnabled, Attachment } from "../../logger"; +import iso from "#braintrust-platform"; +import { + _internalCaptureAttachmentsEnabled, + Attachment, +} from "../../logger/core"; import { processInputAttachments } from "../../wrappers/attachment-utils"; import type { OllamaChatRequest, diff --git a/js/src/instrumentation/providers/openai-agents-api-instrumentation.test.ts b/js/src/instrumentation/providers/openai-agents-api-instrumentation.test.ts index 589c22a82..21ef99b21 100644 --- a/js/src/instrumentation/providers/openai-agents-api-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openai-agents-api-instrumentation.test.ts @@ -1,10 +1,13 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { - _exportsForTestingOnly, - initLogger, - startSpan, - withCurrent, -} from "../../logger"; + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger, startSpan, withCurrent } from "../../logger/core"; import { configureNode } from "../../node/config"; import { failOpenAIAgentsTrace, diff --git a/js/src/instrumentation/providers/openai-agents-api-instrumentation.ts b/js/src/instrumentation/providers/openai-agents-api-instrumentation.ts index ef76dc665..d12094fbc 100644 --- a/js/src/instrumentation/providers/openai-agents-api-instrumentation.ts +++ b/js/src/instrumentation/providers/openai-agents-api-instrumentation.ts @@ -5,7 +5,7 @@ import { NOOP_SPAN, type Span, withCurrent, -} from "../../logger"; +} from "../../logger/core"; import type { OpenAIAgentsOpenTool, OpenAIAgentsTraceState, diff --git a/js/src/instrumentation/providers/openai-agents-channels.ts b/js/src/instrumentation/providers/openai-agents-channels.ts index fb4760869..410f41646 100644 --- a/js/src/instrumentation/providers/openai-agents-channels.ts +++ b/js/src/instrumentation/providers/openai-agents-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -5,25 +9,63 @@ import type { OpenAIAgentsTrace, } from "../../vendor-sdk-types/openai-agents"; -export const openAIAgentsCoreChannels = defineChannels( - "@openai/agents-core", - { - onTraceStart: channel<[OpenAIAgentsTrace], void>({ - channelName: "tracing.processor.onTraceStart", - kind: "async", - }), - onTraceEnd: channel<[OpenAIAgentsTrace], void>({ - channelName: "tracing.processor.onTraceEnd", - kind: "async", - }), - onSpanStart: channel<[OpenAIAgentsSpan], void>({ - channelName: "tracing.processor.onSpanStart", - kind: "async", - }), - onSpanEnd: channel<[OpenAIAgentsSpan], void>({ - channelName: "tracing.processor.onSpanEnd", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.OPENAI_AGENTS }, -); +export const openAIAgentsCoreChannels: { + onTraceStart: MaterializedChannel< + ChannelSpec< + [OpenAIAgentsTrace], + void, + Record, + never, + "async" + > + >; + onTraceEnd: MaterializedChannel< + ChannelSpec< + [OpenAIAgentsTrace], + void, + Record, + never, + "async" + > + >; + onSpanStart: MaterializedChannel< + ChannelSpec< + [OpenAIAgentsSpan], + void, + Record, + never, + "async" + > + >; + onSpanEnd: MaterializedChannel< + ChannelSpec< + [OpenAIAgentsSpan], + void, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@openai/agents-core", + { + onTraceStart: channel<[OpenAIAgentsTrace], void>({ + channelName: "tracing.processor.onTraceStart", + kind: "async", + }), + onTraceEnd: channel<[OpenAIAgentsTrace], void>({ + channelName: "tracing.processor.onTraceEnd", + kind: "async", + }), + onSpanStart: channel<[OpenAIAgentsSpan], void>({ + channelName: "tracing.processor.onSpanStart", + kind: "async", + }), + onSpanEnd: channel<[OpenAIAgentsSpan], void>({ + channelName: "tracing.processor.onSpanEnd", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.OPENAI_AGENTS }, + ))(); diff --git a/js/src/instrumentation/providers/openai-agents-instrumentation.test.ts b/js/src/instrumentation/providers/openai-agents-instrumentation.test.ts index fb6f62fba..f50fb4b60 100644 --- a/js/src/instrumentation/providers/openai-agents-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openai-agents-instrumentation.test.ts @@ -1,6 +1,14 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; import { configureNode } from "../../node/config"; -import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger } from "../../logger/core"; import { configureInstrumentation } from "../registry"; import { openAIAgentsCoreChannels } from "./openai-agents-channels"; import { registerOpenAIAgentsInstrumentation } from "./openai-agents-instrumentation"; diff --git a/js/src/instrumentation/providers/openai-agents-trace-processor.ts b/js/src/instrumentation/providers/openai-agents-trace-processor.ts index 42d5c32b4..57b1f14a7 100644 --- a/js/src/instrumentation/providers/openai-agents-trace-processor.ts +++ b/js/src/instrumentation/providers/openai-agents-trace-processor.ts @@ -7,7 +7,7 @@ import { currentSpan, startSpan as startBaseSpan, withCurrent, -} from "../../logger"; +} from "../../logger/core"; import { processInputAttachments } from "../../wrappers/attachment-utils"; import { processImagesInOutput } from "./openai-span-data"; import { @@ -195,7 +195,14 @@ export class OpenAIAgentsTraceProcessor { >(); private traceOrder: string[] = []; - public readonly _traceSpans = this.traceSpans; + public readonly _traceSpans: Map< + string, + { + rootSpan: BraintrustSpan; + childSpans: Map; + metadata: TraceMetadata; + } + > = this.traceSpans; constructor(options: OpenAIAgentsTraceProcessorOptions = {}) { this.logger = options.logger; diff --git a/js/src/instrumentation/providers/openai-batch-instrumentation.test.ts b/js/src/instrumentation/providers/openai-batch-instrumentation.test.ts index 30ae23295..7901dce41 100644 --- a/js/src/instrumentation/providers/openai-batch-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openai-batch-instrumentation.test.ts @@ -6,13 +6,9 @@ import { describe, expect, it, -} from "vitest"; -import { - _exportsForTestingOnly, - initLogger, - startSpan, - withCurrent, -} from "../../logger"; +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger, startSpan, withCurrent } from "../../logger/core"; import { configureNode } from "../../node/config"; import { completeOpenAIBatchTrace, diff --git a/js/src/instrumentation/providers/openai-batch-instrumentation.ts b/js/src/instrumentation/providers/openai-batch-instrumentation.ts index 378d60865..0e134a123 100644 --- a/js/src/instrumentation/providers/openai-batch-instrumentation.ts +++ b/js/src/instrumentation/providers/openai-batch-instrumentation.ts @@ -6,7 +6,7 @@ import { NOOP_SPAN, type Span, withCurrent, -} from "../../logger"; +} from "../../logger/core"; import { parseMetricsFromUsage } from "../../openai-utils"; import { INSTRUMENTATION_NAMES, diff --git a/js/src/instrumentation/providers/openai-channels.ts b/js/src/instrumentation/providers/openai-channels.ts index 6f6bac9a0..4938db79c 100644 --- a/js/src/instrumentation/providers/openai-channels.ts +++ b/js/src/instrumentation/providers/openai-channels.ts @@ -1,8 +1,12 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import type { OpenAIMediaParams, OpenAIMediaResponse, } from "../../vendor-sdk-types/openai-media"; -import type { CompiledPrompt } from "../../logger"; +import type { CompiledPrompt } from "../../logger/prompts"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { StartOf } from "../core/channel-definitions"; @@ -43,182 +47,373 @@ type OpenAIChannelExtras = type OpenAIChatChannelExtras = OpenAIChannelExtras; type OpenAIResponsesChannelExtras = OpenAIChannelExtras; -export const openAIChannels = defineChannels( - "openai", - { - imagesGenerate: channel< +export const openAIChannels: { + imagesGenerate: MaterializedChannel< + ChannelSpec< [OpenAIMediaParams, unknown?], OpenAIMediaResponse, - OpenAIChannelExtras - >({ channelName: "images.generate", kind: "async" }), - imagesEdit: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + imagesEdit: MaterializedChannel< + ChannelSpec< [OpenAIMediaParams, unknown?], OpenAIMediaResponse, - OpenAIChannelExtras - >({ channelName: "images.edit", kind: "async" }), - imagesCreateVariation: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + imagesCreateVariation: MaterializedChannel< + ChannelSpec< [OpenAIMediaParams, unknown?], OpenAIMediaResponse, - OpenAIChannelExtras - >({ channelName: "images.createVariation", kind: "async" }), - audioSpeechCreate: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + audioSpeechCreate: MaterializedChannel< + ChannelSpec< [OpenAIMediaParams, unknown?], OpenAIMediaResponse, - OpenAIChannelExtras - >({ channelName: "audio.speech.create", kind: "async" }), - audioTranscriptionsCreate: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + audioTranscriptionsCreate: MaterializedChannel< + ChannelSpec< [OpenAIMediaParams, unknown?], OpenAIMediaResponse, - OpenAIChannelExtras - >({ channelName: "audio.transcriptions.create", kind: "async" }), - audioTranslationsCreate: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + audioTranslationsCreate: MaterializedChannel< + ChannelSpec< [OpenAIMediaParams, unknown?], OpenAIMediaResponse, - OpenAIChannelExtras - >({ channelName: "audio.translations.create", kind: "async" }), - - agentsTraceStart: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + agentsTraceStart: MaterializedChannel< + ChannelSpec< [OpenAIAgentsTraceStartChannelArgs], OpenAIAgentsTraceState | null, - OpenAIChannelExtras - >({ - channelName: "agents.trace.start", - kind: "async", - }), - - agentsTraceCapture: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + agentsTraceCapture: MaterializedChannel< + ChannelSpec< [{ event: unknown; state: OpenAIAgentsTraceState | null }], OpenAIAgentsTraceState | null, - OpenAIChannelExtras - >({ - channelName: "agents.trace.capture", - kind: "async", - }), - - agentsTraceFail: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + agentsTraceFail: MaterializedChannel< + ChannelSpec< [{ error: unknown; state: OpenAIAgentsTraceState | null }], OpenAIAgentsTraceState | null, - OpenAIChannelExtras - >({ - channelName: "agents.trace.fail", - kind: "async", - }), - - filesCreateTraced: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + filesCreateTraced: MaterializedChannel< + ChannelSpec< [OpenAIFilesCreateTraceArgs], OpenAIFileLike, - OpenAIChannelExtras - >({ - channelName: "files.create-traced", - kind: "async", - }), - - batchesRetrieveTraced: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + batchesRetrieveTraced: MaterializedChannel< + ChannelSpec< [OpenAIBatchesRetrieveTraceArgs], OpenAIBatchLike, - OpenAIChannelExtras - >({ - channelName: "batches.retrieve-traced", - kind: "async", - }), - - batchesCompleteTrace: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + batchesCompleteTrace: MaterializedChannel< + ChannelSpec< [CompleteOpenAIBatchTraceArgs], void, - OpenAIChannelExtras - >({ - channelName: "batches.complete-trace", - kind: "async", - }), - - chatCompletionsCreate: channel< + OpenAIChannelExtras, + never, + "async" + > + >; + chatCompletionsCreate: MaterializedChannel< + ChannelSpec< [OpenAIChatCreateParams], OpenAIChatCompletion | OpenAIChatStream, OpenAIChatChannelExtras, - OpenAIChatCompletionChunk - >({ - channelName: "chat.completions.create", - kind: "async", - }), - - embeddingsCreate: channel< + OpenAIChatCompletionChunk, + "async" + > + >; + embeddingsCreate: MaterializedChannel< + ChannelSpec< [OpenAIEmbeddingCreateParams], OpenAIEmbeddingResponse, - OpenAIChatChannelExtras - >({ - channelName: "embeddings.create", - kind: "async", - }), - - betaChatCompletionsParse: channel< + OpenAIChatChannelExtras, + never, + "async" + > + >; + betaChatCompletionsParse: MaterializedChannel< + ChannelSpec< [OpenAIChatCreateParams], OpenAIChatCompletion, OpenAIChatChannelExtras, - OpenAIChatCompletionChunk - >({ - channelName: "beta.chat.completions.parse", - kind: "async", - }), - - betaChatCompletionsStream: channel< + OpenAIChatCompletionChunk, + "async" + > + >; + betaChatCompletionsStream: MaterializedChannel< + ChannelSpec< [OpenAIChatCreateParams], unknown, - OpenAIChatChannelExtras - >({ - channelName: "beta.chat.completions.stream", - kind: "sync-stream", - }), - - moderationsCreate: channel< + OpenAIChatChannelExtras, + never, + "sync-stream" + > + >; + moderationsCreate: MaterializedChannel< + ChannelSpec< [OpenAIModerationCreateParams], OpenAIModerationResponse, - OpenAIChatChannelExtras - >({ - channelName: "moderations.create", - kind: "async", - }), - - responsesCreate: channel< + OpenAIChatChannelExtras, + never, + "async" + > + >; + responsesCreate: MaterializedChannel< + ChannelSpec< [OpenAIResponseCreateParams], OpenAIResponse | AsyncIterable, OpenAIResponsesChannelExtras, - OpenAIResponseStreamEvent - >({ - channelName: "responses.create", - kind: "async", - }), - - responsesStream: channel< + OpenAIResponseStreamEvent, + "async" + > + >; + responsesStream: MaterializedChannel< + ChannelSpec< [OpenAIResponseCreateParams], unknown, OpenAIResponsesChannelExtras, - OpenAIResponseStreamEvent - >({ - channelName: "responses.stream", - kind: "sync-stream", - }), - - responsesParse: channel< + OpenAIResponseStreamEvent, + "sync-stream" + > + >; + responsesParse: MaterializedChannel< + ChannelSpec< [OpenAIResponseCreateParams], OpenAIResponse, OpenAIResponsesChannelExtras, - OpenAIResponseStreamEvent - >({ - channelName: "responses.parse", - kind: "async", - }), - - responsesCompact: channel< + OpenAIResponseStreamEvent, + "async" + > + >; + responsesCompact: MaterializedChannel< + ChannelSpec< [OpenAIResponseCompactParams], OpenAIResponse, - OpenAIResponsesChannelExtras - >({ - channelName: "responses.compact", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.OPENAI }, -); + OpenAIResponsesChannelExtras, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "openai", + { + imagesGenerate: channel< + [OpenAIMediaParams, unknown?], + OpenAIMediaResponse, + OpenAIChannelExtras + >({ channelName: "images.generate", kind: "async" }), + imagesEdit: channel< + [OpenAIMediaParams, unknown?], + OpenAIMediaResponse, + OpenAIChannelExtras + >({ channelName: "images.edit", kind: "async" }), + imagesCreateVariation: channel< + [OpenAIMediaParams, unknown?], + OpenAIMediaResponse, + OpenAIChannelExtras + >({ channelName: "images.createVariation", kind: "async" }), + audioSpeechCreate: channel< + [OpenAIMediaParams, unknown?], + OpenAIMediaResponse, + OpenAIChannelExtras + >({ channelName: "audio.speech.create", kind: "async" }), + audioTranscriptionsCreate: channel< + [OpenAIMediaParams, unknown?], + OpenAIMediaResponse, + OpenAIChannelExtras + >({ channelName: "audio.transcriptions.create", kind: "async" }), + audioTranslationsCreate: channel< + [OpenAIMediaParams, unknown?], + OpenAIMediaResponse, + OpenAIChannelExtras + >({ channelName: "audio.translations.create", kind: "async" }), + + agentsTraceStart: channel< + [OpenAIAgentsTraceStartChannelArgs], + OpenAIAgentsTraceState | null, + OpenAIChannelExtras + >({ + channelName: "agents.trace.start", + kind: "async", + }), + + agentsTraceCapture: channel< + [{ event: unknown; state: OpenAIAgentsTraceState | null }], + OpenAIAgentsTraceState | null, + OpenAIChannelExtras + >({ + channelName: "agents.trace.capture", + kind: "async", + }), + + agentsTraceFail: channel< + [{ error: unknown; state: OpenAIAgentsTraceState | null }], + OpenAIAgentsTraceState | null, + OpenAIChannelExtras + >({ + channelName: "agents.trace.fail", + kind: "async", + }), + + filesCreateTraced: channel< + [OpenAIFilesCreateTraceArgs], + OpenAIFileLike, + OpenAIChannelExtras + >({ + channelName: "files.create-traced", + kind: "async", + }), + + batchesRetrieveTraced: channel< + [OpenAIBatchesRetrieveTraceArgs], + OpenAIBatchLike, + OpenAIChannelExtras + >({ + channelName: "batches.retrieve-traced", + kind: "async", + }), + + batchesCompleteTrace: channel< + [CompleteOpenAIBatchTraceArgs], + void, + OpenAIChannelExtras + >({ + channelName: "batches.complete-trace", + kind: "async", + }), + + chatCompletionsCreate: channel< + [OpenAIChatCreateParams], + OpenAIChatCompletion | OpenAIChatStream, + OpenAIChatChannelExtras, + OpenAIChatCompletionChunk + >({ + channelName: "chat.completions.create", + kind: "async", + }), + + embeddingsCreate: channel< + [OpenAIEmbeddingCreateParams], + OpenAIEmbeddingResponse, + OpenAIChatChannelExtras + >({ + channelName: "embeddings.create", + kind: "async", + }), + + betaChatCompletionsParse: channel< + [OpenAIChatCreateParams], + OpenAIChatCompletion, + OpenAIChatChannelExtras, + OpenAIChatCompletionChunk + >({ + channelName: "beta.chat.completions.parse", + kind: "async", + }), + + betaChatCompletionsStream: channel< + [OpenAIChatCreateParams], + unknown, + OpenAIChatChannelExtras + >({ + channelName: "beta.chat.completions.stream", + kind: "sync-stream", + }), + + moderationsCreate: channel< + [OpenAIModerationCreateParams], + OpenAIModerationResponse, + OpenAIChatChannelExtras + >({ + channelName: "moderations.create", + kind: "async", + }), + + responsesCreate: channel< + [OpenAIResponseCreateParams], + OpenAIResponse | AsyncIterable, + OpenAIResponsesChannelExtras, + OpenAIResponseStreamEvent + >({ + channelName: "responses.create", + kind: "async", + }), + + responsesStream: channel< + [OpenAIResponseCreateParams], + unknown, + OpenAIResponsesChannelExtras, + OpenAIResponseStreamEvent + >({ + channelName: "responses.stream", + kind: "sync-stream", + }), + + responsesParse: channel< + [OpenAIResponseCreateParams], + OpenAIResponse, + OpenAIResponsesChannelExtras, + OpenAIResponseStreamEvent + >({ + channelName: "responses.parse", + kind: "async", + }), + + responsesCompact: channel< + [OpenAIResponseCompactParams], + OpenAIResponse, + OpenAIResponsesChannelExtras + >({ + channelName: "responses.compact", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.OPENAI }, + ))(); export type OpenAIChannel = (typeof openAIChannels)[keyof typeof openAIChannels]; diff --git a/js/src/instrumentation/providers/openai-codex-channels.ts b/js/src/instrumentation/providers/openai-codex-channels.ts index bd622fe5c..cc42f159b 100644 --- a/js/src/instrumentation/providers/openai-codex-channels.ts +++ b/js/src/instrumentation/providers/openai-codex-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -9,26 +13,46 @@ import type { OpenAICodexTurnOptions, } from "../../vendor-sdk-types/openai-codex"; -export const openAICodexChannels = defineChannels( - "@openai/codex-sdk", - { - run: channel< +export const openAICodexChannels: { + run: MaterializedChannel< + ChannelSpec< [OpenAICodexInput, OpenAICodexTurnOptions | undefined], OpenAICodexTurn, - { operation?: "run"; thread?: OpenAICodexThread } - >({ - channelName: "Thread.run", - kind: "async", - }), - runStreamed: channel< + { operation?: "run"; thread?: OpenAICodexThread }, + never, + "async" + > + >; + runStreamed: MaterializedChannel< + ChannelSpec< [OpenAICodexInput, OpenAICodexTurnOptions | undefined], OpenAICodexStreamedTurn, { operation?: "runStreamed"; thread?: OpenAICodexThread }, - OpenAICodexThreadEvent - >({ - channelName: "Thread.runStreamed", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.OPENAI_CODEX }, -); + OpenAICodexThreadEvent, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@openai/codex-sdk", + { + run: channel< + [OpenAICodexInput, OpenAICodexTurnOptions | undefined], + OpenAICodexTurn, + { operation?: "run"; thread?: OpenAICodexThread } + >({ + channelName: "Thread.run", + kind: "async", + }), + runStreamed: channel< + [OpenAICodexInput, OpenAICodexTurnOptions | undefined], + OpenAICodexStreamedTurn, + { operation?: "runStreamed"; thread?: OpenAICodexThread }, + OpenAICodexThreadEvent + >({ + channelName: "Thread.runStreamed", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.OPENAI_CODEX }, + ))(); diff --git a/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts b/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts index c919b6d3b..c064702f3 100644 --- a/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openai-codex-instrumentation.test.ts @@ -1,21 +1,28 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; const { mockStartSpan } = vi.hoisted(() => ({ mockStartSpan: vi.fn(), })); -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(), }, })); -vi.mock("../../logger", () => ({ +vi.mock("../../logger/core", () => ({ startSpan: (...args: unknown[]) => mockStartSpan(...args), _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), })); -import iso from "../../isomorph"; +import iso from "#braintrust-platform"; import { registerOpenAICodexInstrumentation } from "./openai-codex-instrumentation"; const mockNewTracingChannel = iso.newTracingChannel as ReturnType; diff --git a/js/src/instrumentation/providers/openai-codex-instrumentation.ts b/js/src/instrumentation/providers/openai-codex-instrumentation.ts index eec4a36a2..fb03e40d4 100644 --- a/js/src/instrumentation/providers/openai-codex-instrumentation.ts +++ b/js/src/instrumentation/providers/openai-codex-instrumentation.ts @@ -1,9 +1,9 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; -import type { IsoChannelHandlers } from "../../isomorph"; +import type { IsoChannelHandlers } from "#braintrust-platform"; import { debugLogger } from "../../debug-logger"; -import { _internalStartSpan as startBaseSpan } from "../../logger"; -import type { Span } from "../../logger"; +import { _internalStartSpan as startBaseSpan } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/openai-instrumentation.test.ts b/js/src/instrumentation/providers/openai-instrumentation.test.ts index 6827b88fe..64b7215c5 100644 --- a/js/src/instrumentation/providers/openai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openai-instrumentation.test.ts @@ -1,11 +1,11 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from "vite-plus/test"; import { parseMetricsFromUsage, aggregateChatCompletionChunks, aggregateResponseStreamEvents, } from "./openai-instrumentation"; import { processImagesInOutput as processImagesInOutputWithFlag } from "./openai-span-data"; -import { Attachment } from "../../logger"; +import { Attachment } from "../../logger/core"; const processImagesInOutput = (output: unknown, captureAttachments = true) => processImagesInOutputWithFlag(output, captureAttachments); diff --git a/js/src/instrumentation/providers/openai-instrumentation.ts b/js/src/instrumentation/providers/openai-instrumentation.ts index 49b3e54bf..ad6661383 100644 --- a/js/src/instrumentation/providers/openai-instrumentation.ts +++ b/js/src/instrumentation/providers/openai-instrumentation.ts @@ -1,4 +1,4 @@ -import { _internalCaptureAttachmentsEnabled } from "../../logger"; +import { _internalCaptureAttachmentsEnabled } from "../../logger/core"; import { processInputAttachments } from "../../wrappers/attachment-utils"; import { interceptOpenAIMedia } from "./openai-media"; import { diff --git a/js/src/instrumentation/providers/openai-media.test.ts b/js/src/instrumentation/providers/openai-media.test.ts index 2232b5945..b97205c73 100644 --- a/js/src/instrumentation/providers/openai-media.test.ts +++ b/js/src/instrumentation/providers/openai-media.test.ts @@ -1,9 +1,12 @@ +import { registerInstrumentation } from "../registrations/openai"; import { Readable } from "node:stream"; -import { afterEach, beforeAll, beforeEach, expect, it } from "vitest"; -import { Attachment, _exportsForTestingOnly, initLogger } from "../../logger"; +import { afterEach, beforeAll, beforeEach, expect, it } from "vite-plus/test"; +import { Attachment, initLogger } from "../../logger/core"; +import { _exportsForTestingOnly } from "../../logger/testing"; import { configureNode } from "../../node/config"; import { openAIChannels } from "./openai-channels"; configureNode(); +registerInstrumentation(); const originalAutoCaptureAttachments = process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; let background: ReturnType< diff --git a/js/src/instrumentation/providers/openai-media.ts b/js/src/instrumentation/providers/openai-media.ts index 3bb8dda73..af45e91b7 100644 --- a/js/src/instrumentation/providers/openai-media.ts +++ b/js/src/instrumentation/providers/openai-media.ts @@ -4,7 +4,7 @@ import { startSpan, withCurrent, type Span, -} from "../../logger"; +} from "../../logger/core"; import { debugLogger } from "../../debug-logger"; import { getCurrentUnixTimestamp } from "../../util"; import { @@ -523,15 +523,7 @@ function observeSpeech( else { observeBytes(result.value); const value = result.value; - chunks.push( - new Blob([ - new Uint8Array( - value.buffer, - value.byteOffset, - value.byteLength, - ), - ]), - ); + chunks.push(new Blob([Uint8Array.from(value)])); } } catch (error) { debugLogger.debug("OpenAI speech capture failed", error); @@ -571,7 +563,7 @@ function observeSpeech( transform(chunk, controller) { try { observeBytes(chunk); - chunks.push(new Blob([chunk])); + chunks.push(new Blob([Uint8Array.from(chunk)])); } catch (error) { debugLogger.debug("OpenAI speech capture failed", error); } @@ -617,7 +609,7 @@ function observeSpeech( transform(chunk, controller) { try { observeBytes(chunk); - chunks.push(new Blob([chunk])); + chunks.push(new Blob([Uint8Array.from(chunk)])); } catch (error) { debugLogger.debug("OpenAI speech capture failed", error); } @@ -638,7 +630,7 @@ function observeSpeech( patchStreamIfNeeded(body, { onChunk: (chunk) => { observeBytes(chunk); - chunks.push(new Blob([chunk])); + chunks.push(new Blob([Uint8Array.from(chunk)])); }, onComplete: () => finish(new Blob(chunks, { type: contentType })), onCancel: () => { diff --git a/js/src/instrumentation/providers/openai-span-data.ts b/js/src/instrumentation/providers/openai-span-data.ts index 9e5635dc8..77a70aee8 100644 --- a/js/src/instrumentation/providers/openai-span-data.ts +++ b/js/src/instrumentation/providers/openai-span-data.ts @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { _internalCaptureAttachmentsEnabled, Attachment } from "../../logger"; +import { + _internalCaptureAttachmentsEnabled, + Attachment, +} from "../../logger/core"; import { omitMediaData, processInputAttachments, @@ -101,7 +104,7 @@ export function extractOpenAIResponsesMetadata( /** Convert Responses API base64 image outputs to Braintrust attachments. */ export function processImagesInOutput( output: any, - captureAttachments = _internalCaptureAttachmentsEnabled(), + captureAttachments: boolean = _internalCaptureAttachmentsEnabled(), ): any { if (Array.isArray(output)) { return output diff --git a/js/src/instrumentation/providers/openrouter-agent-channels.ts b/js/src/instrumentation/providers/openrouter-agent-channels.ts index a8fa85015..a15c70b3e 100644 --- a/js/src/instrumentation/providers/openrouter-agent-channels.ts +++ b/js/src/instrumentation/providers/openrouter-agent-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -5,27 +9,30 @@ import type { OpenRouterAgentCallModelRequest, } from "../../vendor-sdk-types/openrouter-agent"; -export const openRouterAgentChannels = defineChannels( - "@openrouter/agent", - { - callModel: channel({ - channelName: "callModel", - kind: "sync-stream", - }), - - callModelTurn: channel< +export const openRouterAgentChannels: { + callModel: MaterializedChannel< + ChannelSpec< + OpenRouterAgentCallModelArgs, + unknown, + Record, + never, + "sync-stream" + > + >; + callModelTurn: MaterializedChannel< + ChannelSpec< [OpenRouterAgentCallModelRequest | undefined], unknown, { step: number; stepType: "initial" | "continue"; - } - >({ - channelName: "callModel.turn", - kind: "async", - }), - - toolExecute: channel< + }, + never, + "async" + > + >; + toolExecute: MaterializedChannel< + ChannelSpec< [unknown], unknown | AsyncIterable, { @@ -35,11 +42,46 @@ export const openRouterAgentChannels = defineChannels( toolCallId?: string; toolName: string; }, - unknown - >({ - channelName: "tool.execute", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.OPENROUTER_AGENT }, -); + unknown, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@openrouter/agent", + { + callModel: channel({ + channelName: "callModel", + kind: "sync-stream", + }), + + callModelTurn: channel< + [OpenRouterAgentCallModelRequest | undefined], + unknown, + { + step: number; + stepType: "initial" | "continue"; + } + >({ + channelName: "callModel.turn", + kind: "async", + }), + + toolExecute: channel< + [unknown], + unknown | AsyncIterable, + { + span_info?: { + name?: string; + }; + toolCallId?: string; + toolName: string; + }, + unknown + >({ + channelName: "tool.execute", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.OPENROUTER_AGENT }, + ))(); diff --git a/js/src/instrumentation/providers/openrouter-agent-instrumentation.test.ts b/js/src/instrumentation/providers/openrouter-agent-instrumentation.test.ts index 27fa78ca2..e16b62f7a 100644 --- a/js/src/instrumentation/providers/openrouter-agent-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openrouter-agent-instrumentation.test.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../registrations/openrouterAgent"; import { afterEach, beforeAll, @@ -6,9 +7,10 @@ import { expect, it, vi, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../../node/config"; -import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger } from "../../logger/core"; import { openRouterAgentChannels } from "./openrouter-agent-channels"; import { aggregateOpenRouterChatChunks, @@ -21,6 +23,7 @@ const TEST_MODEL = "gpt-4.1-mini"; try { configureNode(); + registerInstrumentation(); } catch { // Best-effort initialization for test environments. } diff --git a/js/src/instrumentation/providers/openrouter-agent-instrumentation.ts b/js/src/instrumentation/providers/openrouter-agent-instrumentation.ts index 7f77cccfc..6b19b500b 100644 --- a/js/src/instrumentation/providers/openrouter-agent-instrumentation.ts +++ b/js/src/instrumentation/providers/openrouter-agent-instrumentation.ts @@ -10,8 +10,8 @@ import { isObject, isPromiseLike, } from "../../../util/index"; -import { withCurrent } from "../../logger"; -import type { Span } from "../../logger"; +import { withCurrent } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { zodToJsonSchema } from "../../zod/utils"; import { openRouterAgentChannels } from "./openrouter-agent-channels"; import type { diff --git a/js/src/instrumentation/providers/openrouter-channels.ts b/js/src/instrumentation/providers/openrouter-channels.ts index bb0bdc4ad..826eba34f 100644 --- a/js/src/instrumentation/providers/openrouter-channels.ts +++ b/js/src/instrumentation/providers/openrouter-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -22,63 +26,66 @@ type OpenRouterResponsesResult = | OpenRouterResponse | AsyncIterable; -export const openRouterChannels = defineChannels( - "@openrouter/sdk", - { - chatSend: channel< +export const openRouterChannels: { + chatSend: MaterializedChannel< + ChannelSpec< [OpenRouterChatCreateParams], OpenRouterChatResult, Record, - OpenRouterChatCompletionChunk - >({ - channelName: "chat.send", - kind: "async", - }), - - embeddingsGenerate: channel< + OpenRouterChatCompletionChunk, + "async" + > + >; + embeddingsGenerate: MaterializedChannel< + ChannelSpec< [OpenRouterEmbeddingCreateParams], - OpenRouterEmbeddingResponse - >({ - channelName: "embeddings.generate", - kind: "async", - }), - - rerankRerank: channel< + OpenRouterEmbeddingResponse, + Record, + never, + "async" + > + >; + rerankRerank: MaterializedChannel< + ChannelSpec< [OpenRouterRerankCreateParams], - OpenRouterRerankResult - >({ - channelName: "rerank.rerank", - kind: "async", - }), - - betaResponsesSend: channel< + OpenRouterRerankResult, + Record, + never, + "async" + > + >; + betaResponsesSend: MaterializedChannel< + ChannelSpec< [OpenRouterResponsesCreateParams], OpenRouterResponsesResult, Record, - OpenRouterResponseStreamEvent - >({ - channelName: "beta.responses.send", - kind: "async", - }), - - callModel: channel<[OpenRouterCallModelRequest], unknown>({ - channelName: "callModel", - kind: "sync-stream", - }), - - callModelTurn: channel< + OpenRouterResponseStreamEvent, + "async" + > + >; + callModel: MaterializedChannel< + ChannelSpec< + [OpenRouterCallModelRequest], + unknown, + Record, + never, + "sync-stream" + > + >; + callModelTurn: MaterializedChannel< + ChannelSpec< [OpenRouterCallModelRequest | undefined], unknown, { step: number; stepType: "initial" | "continue"; - } - >({ - channelName: "callModel.turn", - kind: "async", - }), - - toolExecute: channel< + }, + never, + "async" + > + >; + toolExecute: MaterializedChannel< + ChannelSpec< [unknown], unknown | AsyncIterable, { @@ -88,11 +95,82 @@ export const openRouterChannels = defineChannels( toolCallId?: string; toolName: string; }, - unknown - >({ - channelName: "tool.execute", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.OPENROUTER }, -); + unknown, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@openrouter/sdk", + { + chatSend: channel< + [OpenRouterChatCreateParams], + OpenRouterChatResult, + Record, + OpenRouterChatCompletionChunk + >({ + channelName: "chat.send", + kind: "async", + }), + + embeddingsGenerate: channel< + [OpenRouterEmbeddingCreateParams], + OpenRouterEmbeddingResponse + >({ + channelName: "embeddings.generate", + kind: "async", + }), + + rerankRerank: channel< + [OpenRouterRerankCreateParams], + OpenRouterRerankResult + >({ + channelName: "rerank.rerank", + kind: "async", + }), + + betaResponsesSend: channel< + [OpenRouterResponsesCreateParams], + OpenRouterResponsesResult, + Record, + OpenRouterResponseStreamEvent + >({ + channelName: "beta.responses.send", + kind: "async", + }), + + callModel: channel<[OpenRouterCallModelRequest], unknown>({ + channelName: "callModel", + kind: "sync-stream", + }), + + callModelTurn: channel< + [OpenRouterCallModelRequest | undefined], + unknown, + { + step: number; + stepType: "initial" | "continue"; + } + >({ + channelName: "callModel.turn", + kind: "async", + }), + + toolExecute: channel< + [unknown], + unknown | AsyncIterable, + { + span_info?: { + name?: string; + }; + toolCallId?: string; + toolName: string; + }, + unknown + >({ + channelName: "tool.execute", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.OPENROUTER }, + ))(); diff --git a/js/src/instrumentation/providers/openrouter-instrumentation.test.ts b/js/src/instrumentation/providers/openrouter-instrumentation.test.ts index c656d5e6d..bcfe24d1b 100644 --- a/js/src/instrumentation/providers/openrouter-instrumentation.test.ts +++ b/js/src/instrumentation/providers/openrouter-instrumentation.test.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../registrations/openrouter"; import { afterEach, beforeAll, @@ -6,9 +7,10 @@ import { expect, it, vi, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../../node/config"; -import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger } from "../../logger/core"; import { openRouterChannels } from "./openrouter-channels"; import { aggregateOpenRouterChatChunks, @@ -23,6 +25,7 @@ const TEST_RERANK_MODEL = "rerank-v3.5"; try { configureNode(); + registerInstrumentation(); } catch { // Best-effort initialization for test environments. } diff --git a/js/src/instrumentation/providers/openrouter-instrumentation.ts b/js/src/instrumentation/providers/openrouter-instrumentation.ts index fa8e54aaa..18d592831 100644 --- a/js/src/instrumentation/providers/openrouter-instrumentation.ts +++ b/js/src/instrumentation/providers/openrouter-instrumentation.ts @@ -10,8 +10,8 @@ import { isObject, isPromiseLike, } from "../../../util/index"; -import { withCurrent } from "../../logger"; -import type { Span } from "../../logger"; +import { withCurrent } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { getCurrentUnixTimestamp } from "../../util"; import { zodToJsonSchema } from "../../zod/utils"; import { openRouterChannels } from "./openrouter-channels"; diff --git a/js/src/instrumentation/providers/pi-coding-agent-channels.ts b/js/src/instrumentation/providers/pi-coding-agent-channels.ts index c537618bb..fc837f795 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-channels.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -5,17 +9,28 @@ import type { PiPromptOptions, } from "../../vendor-sdk-types/pi-coding-agent"; -export const piCodingAgentChannels = defineChannels( - "@earendil-works/pi-coding-agent", - { - prompt: channel< +export const piCodingAgentChannels: { + prompt: MaterializedChannel< + ChannelSpec< [string, PiPromptOptions | undefined], void, - { session?: PiAgentSession } - >({ - channelName: "AgentSession.prompt", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.PI_CODING_AGENT }, -); + { session?: PiAgentSession }, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@earendil-works/pi-coding-agent", + { + prompt: channel< + [string, PiPromptOptions | undefined], + void, + { session?: PiAgentSession } + >({ + channelName: "AgentSession.prompt", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.PI_CODING_AGENT }, + ))(); diff --git a/js/src/instrumentation/providers/pi-coding-agent-context.test.ts b/js/src/instrumentation/providers/pi-coding-agent-context.test.ts index c2196932f..2953dbe66 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-context.test.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-context.test.ts @@ -1,10 +1,7 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - _exportsForTestingOnly, - currentSpan, - startSpan, - type Span, -} from "../../logger"; +import { registerInstrumentation } from "../registrations/piCodingAgent"; +import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { currentSpan, startSpan, type Span } from "../../logger/core"; import { configureNode } from "../../node/config"; import type { PiAgentEvent, @@ -17,6 +14,7 @@ import { piCodingAgentChannels } from "./pi-coding-agent-channels"; // Use real Braintrust spans and async context, capturing all log writes in memory. configureNode(); +registerInstrumentation(); describe("Pi tool execution context", () => { let background: ReturnType< diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts index 2002880c3..a8c18190a 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; const { interceptorsByName, @@ -12,8 +19,8 @@ const { mockStartSpan: vi.fn(), })); -vi.mock("../../isomorph", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("#braintrust-platform", async (importOriginal) => { + const actual = await importOriginal(); const { AsyncLocalStorage } = await import("node:async_hooks"); return { ...actual, @@ -25,8 +32,8 @@ vi.mock("../../isomorph", async (importOriginal) => { }; }); -vi.mock("../../logger", async () => { - const { default: iso } = await import("../../isomorph"); +vi.mock("../../logger/core", async () => { + const { default: iso } = await import("#braintrust-platform"); return { _internalCaptureAttachmentsEnabled: () => iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts index 481c1f841..fe5709ab5 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.ts @@ -1,14 +1,14 @@ import { toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; -import iso, { type IsoAsyncLocalStorage } from "../../isomorph"; +import iso, { type IsoAsyncLocalStorage } from "#braintrust-platform"; import { debugLogger } from "../../debug-logger"; import { _internalCaptureAttachmentsEnabled, CAPTURE_ATTACHMENTS, withCurrent, _internalStartSpan as startBaseSpan, -} from "../../logger"; -import type { Span } from "../../logger"; +} from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/strands-agent-sdk-channels.ts b/js/src/instrumentation/providers/strands-agent-sdk-channels.ts index f385f308e..00877bccd 100644 --- a/js/src/instrumentation/providers/strands-agent-sdk-channels.ts +++ b/js/src/instrumentation/providers/strands-agent-sdk-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -17,20 +21,18 @@ type StrandsChannelContext = { self?: unknown; }; -export const strandsAgentSDKChannels = defineChannels( - "@strands-agents/sdk", - { - agentStream: channel< +export const strandsAgentSDKChannels: { + agentStream: MaterializedChannel< + ChannelSpec< [StrandsInvokeArgs, StrandsInvokeOptions | undefined], AsyncGenerator, StrandsChannelContext & { agent?: StrandsAgent }, - StrandsAgentStreamEvent - >({ - channelName: "Agent.stream", - kind: "sync-stream", - }), - - graphStream: channel< + StrandsAgentStreamEvent, + "sync-stream" + > + >; + graphStream: MaterializedChannel< + ChannelSpec< [StrandsMultiAgentInput, StrandsMultiAgentInvokeOptions | undefined], AsyncGenerator< StrandsMultiAgentStreamEvent, @@ -38,13 +40,12 @@ export const strandsAgentSDKChannels = defineChannels( undefined >, StrandsChannelContext & { orchestrator?: StrandsMultiAgent }, - StrandsMultiAgentStreamEvent - >({ - channelName: "Graph.stream", - kind: "sync-stream", - }), - - swarmStream: channel< + StrandsMultiAgentStreamEvent, + "sync-stream" + > + >; + swarmStream: MaterializedChannel< + ChannelSpec< [StrandsMultiAgentInput, StrandsMultiAgentInvokeOptions | undefined], AsyncGenerator< StrandsMultiAgentStreamEvent, @@ -52,11 +53,51 @@ export const strandsAgentSDKChannels = defineChannels( undefined >, StrandsChannelContext & { orchestrator?: StrandsMultiAgent }, - StrandsMultiAgentStreamEvent - >({ - channelName: "Swarm.stream", - kind: "sync-stream", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.STRANDS_AGENT_SDK }, -); + StrandsMultiAgentStreamEvent, + "sync-stream" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@strands-agents/sdk", + { + agentStream: channel< + [StrandsInvokeArgs, StrandsInvokeOptions | undefined], + AsyncGenerator, + StrandsChannelContext & { agent?: StrandsAgent }, + StrandsAgentStreamEvent + >({ + channelName: "Agent.stream", + kind: "sync-stream", + }), + + graphStream: channel< + [StrandsMultiAgentInput, StrandsMultiAgentInvokeOptions | undefined], + AsyncGenerator< + StrandsMultiAgentStreamEvent, + StrandsMultiAgentResult, + undefined + >, + StrandsChannelContext & { orchestrator?: StrandsMultiAgent }, + StrandsMultiAgentStreamEvent + >({ + channelName: "Graph.stream", + kind: "sync-stream", + }), + + swarmStream: channel< + [StrandsMultiAgentInput, StrandsMultiAgentInvokeOptions | undefined], + AsyncGenerator< + StrandsMultiAgentStreamEvent, + StrandsMultiAgentResult, + undefined + >, + StrandsChannelContext & { orchestrator?: StrandsMultiAgent }, + StrandsMultiAgentStreamEvent + >({ + channelName: "Swarm.stream", + kind: "sync-stream", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.STRANDS_AGENT_SDK }, + ))(); diff --git a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts index b29a598ce..351bc24c6 100644 --- a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; const { mockWithCurrent, mockNewAsyncLocalStorage, mockStartSpan } = vi.hoisted( () => ({ @@ -22,7 +29,7 @@ const { mockWithCurrent, mockNewAsyncLocalStorage, mockStartSpan } = vi.hoisted( }), ); -vi.mock("../../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { getEnv: vi.fn((name: string) => name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, @@ -32,7 +39,7 @@ vi.mock("../../isomorph", () => ({ }, })); -vi.mock("../../logger", async (importOriginal) => { +vi.mock("../../logger/core", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, @@ -44,8 +51,8 @@ vi.mock("../../logger", async (importOriginal) => { vi.mock("../../lru-cache", { spy: true }); import { LRUCache } from "../../lru-cache"; -import iso from "../../isomorph"; -import { Attachment } from "../../logger"; +import iso from "#braintrust-platform"; +import { Attachment } from "../../logger/core"; import { isAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; import { registerStrandsAgentSDKInstrumentation } from "./strands-agent-sdk-instrumentation"; diff --git a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts index c3b791078..a85ba4b2f 100644 --- a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts +++ b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.ts @@ -7,8 +7,8 @@ import { BaseAttachment, startSpan as startBaseSpan, withCurrent, -} from "../../logger"; -import type { Span } from "../../logger"; +} from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/typesafe-channels.ts b/js/src/instrumentation/providers/typesafe-channels.ts index 5c54fa41b..da3545ed7 100644 --- a/js/src/instrumentation/providers/typesafe-channels.ts +++ b/js/src/instrumentation/providers/typesafe-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { TypeSafeSystemOneRequest, @@ -5,16 +9,27 @@ import type { } from "../../vendor-sdk-types/typesafe"; import { channel, defineChannels } from "../core/channel-definitions"; -export const typeSafeChannels = defineChannels( - "@typesafe-ai/sdk", - { - systemOne: channel< +export const typeSafeChannels: { + systemOne: MaterializedChannel< + ChannelSpec< [TypeSafeSystemOneRequest, options?: unknown], - TypeSafeSystemOneResult - >({ - channelName: "systemOne", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.TYPESAFE }, -); + TypeSafeSystemOneResult, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "@typesafe-ai/sdk", + { + systemOne: channel< + [TypeSafeSystemOneRequest, options?: unknown], + TypeSafeSystemOneResult + >({ + channelName: "systemOne", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.TYPESAFE }, + ))(); diff --git a/js/src/instrumentation/providers/typesafe-instrumentation.ts b/js/src/instrumentation/providers/typesafe-instrumentation.ts index 591ffe9de..f936f742a 100644 --- a/js/src/instrumentation/providers/typesafe-instrumentation.ts +++ b/js/src/instrumentation/providers/typesafe-instrumentation.ts @@ -1,7 +1,7 @@ import { isObject } from "../../../util/index"; import { debugLogger } from "../../debug-logger"; -import { startSpan, withCurrent } from "../../logger"; -import type { Span } from "../../logger"; +import { startSpan, withCurrent } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/providers/voyageai-channels.ts b/js/src/instrumentation/providers/voyageai-channels.ts index 757337007..ec961ae2f 100644 --- a/js/src/instrumentation/providers/voyageai-channels.ts +++ b/js/src/instrumentation/providers/voyageai-channels.ts @@ -1,3 +1,7 @@ +import type { + MaterializedChannel, + ChannelSpec, +} from "../core/channel-definitions"; import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; import type { @@ -10,40 +14,78 @@ import type { VoyageAIRerankResponse, } from "../../vendor-sdk-types/voyageai"; -export const voyageAIChannels = defineChannels( - "voyageai", - { - embed: channel< +export const voyageAIChannels: { + embed: MaterializedChannel< + ChannelSpec< [VoyageAIEmbedRequest, options?: unknown], - VoyageAIEmbeddingResponse - >({ - channelName: "embed", - kind: "async", - }), - - multimodalEmbed: channel< + VoyageAIEmbeddingResponse, + Record, + never, + "async" + > + >; + multimodalEmbed: MaterializedChannel< + ChannelSpec< [VoyageAIMultimodalEmbedRequest, options?: unknown], - VoyageAIEmbeddingResponse - >({ - channelName: "multimodalEmbed", - kind: "async", - }), - - rerank: channel< + VoyageAIEmbeddingResponse, + Record, + never, + "async" + > + >; + rerank: MaterializedChannel< + ChannelSpec< [VoyageAIRerankRequest, options?: unknown], - VoyageAIRerankResponse - >({ - channelName: "rerank", - kind: "async", - }), - - contextualizedEmbed: channel< + VoyageAIRerankResponse, + Record, + never, + "async" + > + >; + contextualizedEmbed: MaterializedChannel< + ChannelSpec< [VoyageAIContextualizedEmbedRequest, options?: unknown], - VoyageAIContextualizedResult - >({ - channelName: "contextualizedEmbed", - kind: "async", - }), - }, - { instrumentationName: INSTRUMENTATION_NAMES.VOYAGEAI }, -); + VoyageAIContextualizedResult, + Record, + never, + "async" + > + >; +} = /* @__PURE__ */ (() => + defineChannels( + "voyageai", + { + embed: channel< + [VoyageAIEmbedRequest, options?: unknown], + VoyageAIEmbeddingResponse + >({ + channelName: "embed", + kind: "async", + }), + + multimodalEmbed: channel< + [VoyageAIMultimodalEmbedRequest, options?: unknown], + VoyageAIEmbeddingResponse + >({ + channelName: "multimodalEmbed", + kind: "async", + }), + + rerank: channel< + [VoyageAIRerankRequest, options?: unknown], + VoyageAIRerankResponse + >({ + channelName: "rerank", + kind: "async", + }), + + contextualizedEmbed: channel< + [VoyageAIContextualizedEmbedRequest, options?: unknown], + VoyageAIContextualizedResult + >({ + channelName: "contextualizedEmbed", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.VOYAGEAI }, + ))(); diff --git a/js/src/instrumentation/providers/voyageai-instrumentation.test.ts b/js/src/instrumentation/providers/voyageai-instrumentation.test.ts index 874b76472..83b956a2f 100644 --- a/js/src/instrumentation/providers/voyageai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/voyageai-instrumentation.test.ts @@ -1,10 +1,20 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { Attachment, _exportsForTestingOnly, initLogger } from "../../logger"; +import { registerInstrumentation } from "../registrations/voyageai"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vite-plus/test"; +import { Attachment, initLogger } from "../../logger/core"; +import { _exportsForTestingOnly } from "../../logger/testing"; import { configureNode } from "../../node/config"; import { voyageAIChannels } from "./voyageai-channels"; try { configureNode(); + registerInstrumentation(); } catch { // Best-effort initialization for test environments. } diff --git a/js/src/instrumentation/providers/voyageai-instrumentation.ts b/js/src/instrumentation/providers/voyageai-instrumentation.ts index ac2304e81..f57e372c2 100644 --- a/js/src/instrumentation/providers/voyageai-instrumentation.ts +++ b/js/src/instrumentation/providers/voyageai-instrumentation.ts @@ -1,7 +1,7 @@ import { SpanTypeAttribute, isObject } from "../../../util/index"; import { debugLogger } from "../../debug-logger"; -import { startSpan, withCurrent } from "../../logger"; -import type { Span } from "../../logger"; +import { startSpan, withCurrent } from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/instrumentation/registrations/aisdk.ts b/js/src/instrumentation/registrations/aisdk.ts new file mode 100644 index 000000000..410929c81 --- /dev/null +++ b/js/src/instrumentation/registrations/aisdk.ts @@ -0,0 +1,9 @@ +import { registerAISDKInstrumentation } from "../providers/ai-sdk-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("aisdk", () => { + registerAISDKInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/anthropic.ts b/js/src/instrumentation/registrations/anthropic.ts new file mode 100644 index 000000000..ca1165dad --- /dev/null +++ b/js/src/instrumentation/registrations/anthropic.ts @@ -0,0 +1,9 @@ +import { registerAnthropicInstrumentation } from "../providers/anthropic-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("anthropic", () => { + registerAnthropicInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/awsBedrockRuntime.ts b/js/src/instrumentation/registrations/awsBedrockRuntime.ts new file mode 100644 index 000000000..d79a7b144 --- /dev/null +++ b/js/src/instrumentation/registrations/awsBedrockRuntime.ts @@ -0,0 +1,9 @@ +import { registerBedrockRuntimeInstrumentation } from "../providers/bedrock-runtime-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("awsBedrockRuntime", () => { + registerBedrockRuntimeInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/claudeAgentSDK.ts b/js/src/instrumentation/registrations/claudeAgentSDK.ts new file mode 100644 index 000000000..36eacf248 --- /dev/null +++ b/js/src/instrumentation/registrations/claudeAgentSDK.ts @@ -0,0 +1,9 @@ +import { registerClaudeAgentSDKInstrumentation } from "../providers/claude-agent-sdk-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("claudeAgentSDK", () => { + registerClaudeAgentSDKInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/cloudflareAIChat.ts b/js/src/instrumentation/registrations/cloudflareAIChat.ts new file mode 100644 index 000000000..81eae531f --- /dev/null +++ b/js/src/instrumentation/registrations/cloudflareAIChat.ts @@ -0,0 +1,9 @@ +import { registerCloudflareAIChatInstrumentation } from "../providers/cloudflare-ai-chat-consumer"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("cloudflareAIChat", () => { + registerCloudflareAIChatInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/cloudflareAgents.ts b/js/src/instrumentation/registrations/cloudflareAgents.ts new file mode 100644 index 000000000..91125edaf --- /dev/null +++ b/js/src/instrumentation/registrations/cloudflareAgents.ts @@ -0,0 +1,9 @@ +import { registerCloudflareAgentsInstrumentation } from "../providers/cloudflare-agents-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("cloudflareAgents", () => { + registerCloudflareAgentsInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/cloudflareThink.ts b/js/src/instrumentation/registrations/cloudflareThink.ts new file mode 100644 index 000000000..0239af4b8 --- /dev/null +++ b/js/src/instrumentation/registrations/cloudflareThink.ts @@ -0,0 +1,9 @@ +import { registerCloudflareThinkInstrumentation } from "../providers/cloudflare-think-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("cloudflareThink", () => { + registerCloudflareThinkInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/cohere.ts b/js/src/instrumentation/registrations/cohere.ts new file mode 100644 index 000000000..68624c09e --- /dev/null +++ b/js/src/instrumentation/registrations/cohere.ts @@ -0,0 +1,9 @@ +import { registerCohereInstrumentation } from "../providers/cohere-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("cohere", () => { + registerCohereInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/cursorSDK.ts b/js/src/instrumentation/registrations/cursorSDK.ts new file mode 100644 index 000000000..116b94a7e --- /dev/null +++ b/js/src/instrumentation/registrations/cursorSDK.ts @@ -0,0 +1,9 @@ +import { registerCursorSDKInstrumentation } from "../providers/cursor-sdk-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("cursorSDK", () => { + registerCursorSDKInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/elevenlabs.ts b/js/src/instrumentation/registrations/elevenlabs.ts new file mode 100644 index 000000000..f6180fb0f --- /dev/null +++ b/js/src/instrumentation/registrations/elevenlabs.ts @@ -0,0 +1,9 @@ +import { registerElevenLabsInstrumentation } from "../providers/elevenlabs-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("elevenlabs", () => { + registerElevenLabsInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/genkit.ts b/js/src/instrumentation/registrations/genkit.ts new file mode 100644 index 000000000..673253430 --- /dev/null +++ b/js/src/instrumentation/registrations/genkit.ts @@ -0,0 +1,9 @@ +import { registerGenkitInstrumentation } from "../providers/genkit-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("genkit", () => { + registerGenkitInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/gitHubCopilot.ts b/js/src/instrumentation/registrations/gitHubCopilot.ts new file mode 100644 index 000000000..9e247c50d --- /dev/null +++ b/js/src/instrumentation/registrations/gitHubCopilot.ts @@ -0,0 +1,9 @@ +import { registerGitHubCopilotInstrumentation } from "../providers/github-copilot-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("gitHubCopilot", () => { + registerGitHubCopilotInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/googleADK.ts b/js/src/instrumentation/registrations/googleADK.ts new file mode 100644 index 000000000..6704bd84b --- /dev/null +++ b/js/src/instrumentation/registrations/googleADK.ts @@ -0,0 +1,9 @@ +import { registerGoogleADKInstrumentation } from "../providers/google-adk-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("googleADK", () => { + registerGoogleADKInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/googleGenAI.ts b/js/src/instrumentation/registrations/googleGenAI.ts new file mode 100644 index 000000000..441da2c35 --- /dev/null +++ b/js/src/instrumentation/registrations/googleGenAI.ts @@ -0,0 +1,9 @@ +import { registerGoogleGenAIInstrumentation } from "../providers/google-genai-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("googleGenAI", () => { + registerGoogleGenAIInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/googleGenerativeAI.ts b/js/src/instrumentation/registrations/googleGenerativeAI.ts new file mode 100644 index 000000000..ffceccbfd --- /dev/null +++ b/js/src/instrumentation/registrations/googleGenerativeAI.ts @@ -0,0 +1,9 @@ +import { registerGoogleGenerativeAIInstrumentation } from "../providers/google-generative-ai-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("googleGenerativeAI", () => { + registerGoogleGenerativeAIInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/groq.ts b/js/src/instrumentation/registrations/groq.ts new file mode 100644 index 000000000..6acdab20d --- /dev/null +++ b/js/src/instrumentation/registrations/groq.ts @@ -0,0 +1,9 @@ +import { registerGroqInstrumentation } from "../providers/groq-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("groq", () => { + registerGroqInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/huggingface.ts b/js/src/instrumentation/registrations/huggingface.ts new file mode 100644 index 000000000..74eee8a31 --- /dev/null +++ b/js/src/instrumentation/registrations/huggingface.ts @@ -0,0 +1,11 @@ +import { registerHuggingFaceInstrumentation } from "../providers/huggingface-instrumentation"; +import { registerHuggingFaceTransformersInstrumentation } from "../providers/huggingface-transformers-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("huggingface", () => { + registerHuggingFaceInstrumentation(); + registerHuggingFaceTransformersInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/langchain.ts b/js/src/instrumentation/registrations/langchain.ts new file mode 100644 index 000000000..3a9b6d4df --- /dev/null +++ b/js/src/instrumentation/registrations/langchain.ts @@ -0,0 +1,9 @@ +import { registerLangChainInstrumentation } from "../providers/langchain-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("langchain", () => { + registerLangChainInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/langgraphSDK.ts b/js/src/instrumentation/registrations/langgraphSDK.ts new file mode 100644 index 000000000..38a2190c2 --- /dev/null +++ b/js/src/instrumentation/registrations/langgraphSDK.ts @@ -0,0 +1,9 @@ +import { registerLangGraphSDKInstrumentation } from "../providers/langgraph-sdk-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("langgraphSDK", () => { + registerLangGraphSDKInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/langsmith.ts b/js/src/instrumentation/registrations/langsmith.ts new file mode 100644 index 000000000..7de06fbd8 --- /dev/null +++ b/js/src/instrumentation/registrations/langsmith.ts @@ -0,0 +1,11 @@ +import { registerLangSmithInstrumentation } from "../providers/langsmith-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("langsmith", () => { + registerLangSmithInstrumentation({ + skipLangChainRuns: () => registry.isRegistered("langchain"), + }); + }); +} diff --git a/js/src/instrumentation/registrations/mastra.ts b/js/src/instrumentation/registrations/mastra.ts new file mode 100644 index 000000000..e909f167f --- /dev/null +++ b/js/src/instrumentation/registrations/mastra.ts @@ -0,0 +1,9 @@ +import { registry } from "../registry"; +import { installMastraExporterFactory } from "../../auto-instrumentations/loader/mastra-observability-patch"; +import { BraintrustObservabilityExporter } from "../../wrappers/mastra"; + +export function registerInstrumentation(): void { + registry.register("mastra", () => { + installMastraExporterFactory(() => new BraintrustObservabilityExporter()); + }); +} diff --git a/js/src/instrumentation/registrations/mistral.ts b/js/src/instrumentation/registrations/mistral.ts new file mode 100644 index 000000000..b1419a1d2 --- /dev/null +++ b/js/src/instrumentation/registrations/mistral.ts @@ -0,0 +1,9 @@ +import { registerMistralInstrumentation } from "../providers/mistral-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("mistral", () => { + registerMistralInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/ollama.ts b/js/src/instrumentation/registrations/ollama.ts new file mode 100644 index 000000000..ff558f6e5 --- /dev/null +++ b/js/src/instrumentation/registrations/ollama.ts @@ -0,0 +1,9 @@ +import { registerOllamaInstrumentation } from "../providers/ollama-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("ollama", () => { + registerOllamaInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/openAIAgents.ts b/js/src/instrumentation/registrations/openAIAgents.ts new file mode 100644 index 000000000..601ff2061 --- /dev/null +++ b/js/src/instrumentation/registrations/openAIAgents.ts @@ -0,0 +1,9 @@ +import { registerOpenAIAgentsInstrumentation } from "../providers/openai-agents-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("openAIAgents", () => { + registerOpenAIAgentsInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/openai.ts b/js/src/instrumentation/registrations/openai.ts new file mode 100644 index 000000000..fcd7f2549 --- /dev/null +++ b/js/src/instrumentation/registrations/openai.ts @@ -0,0 +1,9 @@ +import { registerOpenAIInstrumentation } from "../providers/openai-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("openai", () => { + registerOpenAIInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/openaiCodexSDK.ts b/js/src/instrumentation/registrations/openaiCodexSDK.ts new file mode 100644 index 000000000..965ea72de --- /dev/null +++ b/js/src/instrumentation/registrations/openaiCodexSDK.ts @@ -0,0 +1,9 @@ +import { registerOpenAICodexInstrumentation } from "../providers/openai-codex-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("openaiCodexSDK", () => { + registerOpenAICodexInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/openrouter.ts b/js/src/instrumentation/registrations/openrouter.ts new file mode 100644 index 000000000..55cc46c86 --- /dev/null +++ b/js/src/instrumentation/registrations/openrouter.ts @@ -0,0 +1,9 @@ +import { registerOpenRouterInstrumentation } from "../providers/openrouter-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("openrouter", () => { + registerOpenRouterInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/openrouterAgent.ts b/js/src/instrumentation/registrations/openrouterAgent.ts new file mode 100644 index 000000000..cafc44873 --- /dev/null +++ b/js/src/instrumentation/registrations/openrouterAgent.ts @@ -0,0 +1,9 @@ +import { registerOpenRouterAgentInstrumentation } from "../providers/openrouter-agent-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("openrouterAgent", () => { + registerOpenRouterAgentInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/piCodingAgent.ts b/js/src/instrumentation/registrations/piCodingAgent.ts new file mode 100644 index 000000000..d3f15a3b8 --- /dev/null +++ b/js/src/instrumentation/registrations/piCodingAgent.ts @@ -0,0 +1,9 @@ +import { registerPiCodingAgentInstrumentation } from "../providers/pi-coding-agent-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("piCodingAgent", () => { + registerPiCodingAgentInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/strandsAgentSDK.ts b/js/src/instrumentation/registrations/strandsAgentSDK.ts new file mode 100644 index 000000000..f149be10d --- /dev/null +++ b/js/src/instrumentation/registrations/strandsAgentSDK.ts @@ -0,0 +1,9 @@ +import { registerStrandsAgentSDKInstrumentation } from "../providers/strands-agent-sdk-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("strandsAgentSDK", () => { + registerStrandsAgentSDKInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/typesafe.ts b/js/src/instrumentation/registrations/typesafe.ts new file mode 100644 index 000000000..20752f2d8 --- /dev/null +++ b/js/src/instrumentation/registrations/typesafe.ts @@ -0,0 +1,9 @@ +import { registerTypeSafeInstrumentation } from "../providers/typesafe-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("typesafe", () => { + registerTypeSafeInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registrations/voyageai.ts b/js/src/instrumentation/registrations/voyageai.ts new file mode 100644 index 000000000..565bbd9cf --- /dev/null +++ b/js/src/instrumentation/registrations/voyageai.ts @@ -0,0 +1,9 @@ +import { registerVoyageAIInstrumentation } from "../providers/voyageai-instrumentation"; +import { registry } from "../registry"; + +/** Enable this provider once across compatible SDK copies. */ +export function registerInstrumentation(): void { + registry.register("voyageai", () => { + registerVoyageAIInstrumentation(); + }); +} diff --git a/js/src/instrumentation/registry.test.ts b/js/src/instrumentation/registry.test.ts index 4606a7b1e..3ef3bb90f 100644 --- a/js/src/instrumentation/registry.test.ts +++ b/js/src/instrumentation/registry.test.ts @@ -1,242 +1,88 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; - -// Mock iso's newTracingChannel - must be before any imports that use it -vi.mock("../isomorph", () => ({ - default: { - newTracingChannel: vi.fn(), - newAsyncLocalStorage: vi.fn(() => ({ - getStore: vi.fn(() => undefined), - run: vi.fn((_store: unknown, callback: () => unknown) => callback()), - })), - getEnv: vi.fn(() => undefined), - buildType: "node", - }, -})); - -import { registry, configureInstrumentation } from "./registry"; -import iso from "../isomorph"; - -const mockNewTracingChannel = iso.newTracingChannel as ReturnType; - -describe("Instrumentation Registry", () => { - beforeEach(() => { - // Setup mock channel - const mockChannel = { - subscribe: vi.fn(), - hasSubscribers: false, - }; - mockNewTracingChannel.mockReturnValue(mockChannel); - }); - - it("should not be enabled by default", () => { - const testRegistry = new (registry.constructor as any)(); - expect(testRegistry.isEnabled()).toBe(false); - }); - - it("should enable instrumentation consumers when enable() is called", () => { - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - expect(testRegistry.isEnabled()).toBe(true); - }); - - it("should be idempotent (calling enable() multiple times)", () => { - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - testRegistry.enable(); // Should not throw - expect(testRegistry.isEnabled()).toBe(true); - }); - - it("should block a second instance from subscribing when another is already enabled", () => { - // Regression test for BT-5139: when the SDK is loaded from two different - // module paths in the same process, each gets its own instrumentation registry - // instance. Without cross-instance deduplication, both would subscribe to - // the same global hook, causing every OpenAI call to produce two - // LLM spans. - // - // The dedup mechanism checks globalThis[Symbol.for("braintrust-state")], - // which is the shared state object that all SDK instances reuse (see - // _internalSetInitialState in logger.ts). We simulate that here. - const sharedState = {}; - const stateKey = Symbol.for("braintrust-state"); - (globalThis as any)[stateKey] = sharedState; - - const instanceA = new (registry.constructor as any)(); - const instanceB = new (registry.constructor as any)(); - - try { - instanceA.enable(); - expect(instanceA.isEnabled()).toBe(true); - - // instanceB should be blocked — the hook is already subscribed - instanceB.enable(); - expect(instanceB.isEnabled()).toBe(false); - } finally { - delete (globalThis as any)[stateKey]; - } - }); - - it("should ignore deduplication markers from older transports", () => { - const stateKey = Symbol.for("braintrust-state"); - (globalThis as any)[stateKey] = { - [Symbol.for("braintrust.registry")]: {}, - }; - const testRegistry = new (registry.constructor as any)(); - - try { - testRegistry.enable(); - expect(testRegistry.isEnabled()).toBe(true); - } finally { - delete (globalThis as any)[stateKey]; - } - }); - - it("should warn if configureInstrumentation is called after enable", () => { - const testRegistry = new (registry.constructor as any)(); - const warnSpy = [] as string[]; - const originalWarn = console.warn; - console.warn = (msg: string) => warnSpy.push(msg); - - try { - testRegistry.enable(); - testRegistry.configure({ integrations: { openai: false } }); - - expect(warnSpy.length).toBeGreaterThan(0); - expect(warnSpy[0]).toContain("Cannot configure instrumentation"); - } finally { - console.warn = originalWarn; - } - }); - - it("should allow configuration before enable", () => { - const testRegistry = new (registry.constructor as any)(); - const warnSpy = [] as string[]; - const originalWarn = console.warn; - console.warn = (msg: string) => warnSpy.push(msg); - - try { - testRegistry.configure({ integrations: { openai: false } }); - testRegistry.enable(); - - expect(warnSpy.length).toBe(0); - } finally { - console.warn = originalWarn; - } - }); +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; +import iso from "#braintrust-platform"; +import { GLOBAL_INSTRUMENTATION_HOOKS_PROTOCOL_VERSION } from "../global-instrumentation-hooks"; +import { configureInstrumentation, registry } from "./registry"; + +const registryKey = Symbol.for( + `braintrust.registry.global-hooks.v${GLOBAL_INSTRUMENTATION_HOOKS_PROTOCOL_VERSION}`, +); + +beforeEach(() => { + delete (globalThis as Record)[registryKey]; }); - -describe("configureInstrumentation API", () => { - it("should export configureInstrumentation function", () => { - expect(typeof configureInstrumentation).toBe("function"); - }); - - it("should accept integration configuration", () => { - // Should not throw - configureInstrumentation({ - integrations: { - openai: false, - openaiCodexSDK: false, - anthropic: true, - huggingface: true, - openrouter: false, - mistral: false, - cohere: false, - mastra: false, - strandsAgentSDK: false, - cloudflareAIChat: false, - cloudflareAgents: false, - }, - }); - }); +afterEach(() => { + vi.restoreAllMocks(); + delete (globalThis as Record)[registryKey]; }); -describe("Environment Variable Configuration", () => { - let originalGetEnv: typeof import("../isomorph").default.getEnv; - - beforeEach(async () => { - const iso = (await import("../isomorph")).default; - originalGetEnv = iso.getEnv; - }); - - afterEach(async () => { - const iso = (await import("../isomorph")).default; - iso.getEnv = originalGetEnv; - }); - - it("should parse BRAINTRUST_DISABLE_INSTRUMENTATION with single SDK", async () => { - const iso = (await import("../isomorph")).default; - iso.getEnv = (name: string) => { - if (name === "BRAINTRUST_DISABLE_INSTRUMENTATION") { - return "openai"; - } - return originalGetEnv(name); - }; - - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - - // OpenAI should be disabled, others enabled by default - expect(testRegistry.isEnabled()).toBe(true); - }); - - it("should parse BRAINTRUST_DISABLE_INSTRUMENTATION with multiple SDKs", async () => { - const iso = (await import("../isomorph")).default; - iso.getEnv = (name: string) => { - if (name === "BRAINTRUST_DISABLE_INSTRUMENTATION") { - return "openai,anthropic"; - } - return originalGetEnv(name); - }; - - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - - // Both should be disabled - expect(testRegistry.isEnabled()).toBe(true); - }); - - it("should handle whitespace in BRAINTRUST_DISABLE_INSTRUMENTATION", async () => { - const iso = (await import("../isomorph")).default; - iso.getEnv = (name: string) => { - if (name === "BRAINTRUST_DISABLE_INSTRUMENTATION") { - return " openai , anthropic , vercel "; - } - return originalGetEnv(name); - }; - - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - - expect(testRegistry.isEnabled()).toBe(true); - }); - - it("should handle empty BRAINTRUST_DISABLE_INSTRUMENTATION", async () => { - const iso = (await import("../isomorph")).default; - iso.getEnv = (name: string) => { - if (name === "BRAINTRUST_DISABLE_INSTRUMENTATION") { - return ""; - } - return originalGetEnv(name); - }; - - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - - // All should be enabled (nothing disabled) - expect(testRegistry.isEnabled()).toBe(true); - }); - - it("should be case-insensitive for SDK names", async () => { - const iso = (await import("../isomorph")).default; - iso.getEnv = (name: string) => { - if (name === "BRAINTRUST_DISABLE_INSTRUMENTATION") { - return "OpenAI,ANTHROPIC"; - } - return originalGetEnv(name); - }; - - const testRegistry = new (registry.constructor as any)(); - testRegistry.enable(); - - expect(testRegistry.isEnabled()).toBe(true); +describe("explicit instrumentation registration", () => { + it("does not register until requested", () => { + expect(registry.isEnabled()).toBe(false); + }); + + it("deduplicates each provider independently across registry copies", () => { + const second = new (registry.constructor as new () => typeof registry)(); + const openai = vi.fn(); + const anthropic = vi.fn(); + registry.register("openai", openai); + second.register("openai", openai); + second.register("anthropic", anthropic); + registry.register("anthropic", anthropic); + expect(openai).toHaveBeenCalledOnce(); + expect(anthropic).toHaveBeenCalledOnce(); + }); + + it("handles reentrant registration", () => { + const subscribe = vi.fn<() => void>(); + subscribe.mockImplementation(() => registry.register("openai", subscribe)); + registry.register("openai", subscribe); + expect(subscribe).toHaveBeenCalledOnce(); + }); + + it("shares configuration before registration and merges provider settings", () => { + configureInstrumentation({ integrations: { openai: false } }); + configureInstrumentation({ integrations: { anthropic: false } }); + const subscribe = vi.fn(); + registry.register("openai", subscribe); + registry.register("anthropic", subscribe); + expect(subscribe).not.toHaveBeenCalled(); + registry.register("groq", subscribe); + expect(subscribe).toHaveBeenCalledOnce(); + }); + + it("warns when configuration arrives after registration", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + registry.register("openai", () => {}); + configureInstrumentation({ integrations: { openai: false } }); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("Cannot configure"), + ); + }); + + it.each(["openai", " OPENAI , anthropic ", "OpenAI,ai-sdk"])( + "environment opt-outs override explicit enablement: %s", + (disabled) => { + vi.spyOn(iso, "getEnv").mockImplementation((name) => + name === "BRAINTRUST_DISABLE_INSTRUMENTATION" ? disabled : undefined, + ); + configureInstrumentation({ integrations: { openai: true } }); + const subscribe = vi.fn(); + registry.register("openai", subscribe); + expect(subscribe).not.toHaveBeenCalled(); + }, + ); + + it("keeps the combined LangChain/LangGraph opt-out", () => { + configureInstrumentation({ integrations: { langgraph: false } }); + const subscribe = vi.fn(); + registry.register("langchain", subscribe); + expect(subscribe).not.toHaveBeenCalled(); }); }); diff --git a/js/src/instrumentation/registry.ts b/js/src/instrumentation/registry.ts index 0e041207d..5dda31d82 100644 --- a/js/src/instrumentation/registry.ts +++ b/js/src/instrumentation/registry.ts @@ -1,164 +1,92 @@ -/** - * Registry and configuration for auto-instrumentation. - * - * Instrumentation consumers are automatically enabled when Braintrust loads. - * Users can disable specific integrations programmatically or via environment variables. - */ - -import { registerInstrumentationConsumers } from "./instrumentation-consumers"; -import iso from "../isomorph"; +import iso from "#braintrust-platform"; import { - getDefaultInstrumentationIntegrations, readDisabledInstrumentationEnvConfig, type InstrumentationConfig, + type InstrumentationIntegrationsConfig, } from "./config"; import { GLOBAL_INSTRUMENTATION_HOOKS_PROTOCOL_VERSION } from "../global-instrumentation-hooks"; +import { installGlobalHookErrorReporter } from "../debug-logger"; import { setSpanCustomizers } from "../span-customizer"; export type { InstrumentationConfig } from "./config"; -// Key used to stamp the active InstrumentationRegistry instance onto the shared -// braintrust state object (globalThis[Symbol.for("braintrust-state")]). -// -// The braintrust state is already shared across all SDK instances loaded in -// the same process (see _internalSetInitialState in logger.ts), so using it -// as the carrier gives us cross-instance deduplication for free: -// -// - BT-5139 scenario: two SDK instances share the same state object → the -// second instance sees the marker left by the first and skips subscription, -// preventing duplicate global hook listeners. -// -// - vi.resetModules() test scenario: the test deletes the state from -// globalThis between runs, so the next import creates a fresh state with no -// marker and can subscribe normally. -const REGISTRY_STATE_KEY = Symbol.for( +const REGISTRY_KEY = Symbol.for( `braintrust.registry.global-hooks.v${GLOBAL_INSTRUMENTATION_HOOKS_PROTOCOL_VERSION}`, ); -function getSharedState(): Record | undefined { - const state = (globalThis as Record)[ - Symbol.for("braintrust-state") - ]; - return state && typeof state === "object" - ? (state as Record) - : undefined; +type Provider = keyof InstrumentationIntegrationsConfig; +interface RegistryState { + config: InstrumentationConfig; + registered: Set; } -class InstrumentationRegistry { - private config: InstrumentationConfig = {}; - private enabled = false; +// Allocate only on explicit configuration or registration, independently of +// logger state. Multiple SDK copies must agree before either creates a logger. +function getRegistryState(): RegistryState { + const root = globalThis as typeof globalThis & { + [REGISTRY_KEY]?: RegistryState; + }; + return (root[REGISTRY_KEY] ??= { config: {}, registered: new Set() }); +} - /** - * Configure which integrations should be enabled. - * This must be called before any SDK imports to take effect. - */ +class InstrumentationRegistry { configure(config: InstrumentationConfig): void { - if (this.enabled) { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. + const state = getRegistryState(); + if (state.registered.size > 0) { + // eslint-disable-next-line no-restricted-properties -- intentional configuration diagnostic. console.warn( "Braintrust: Cannot configure instrumentation after it has been enabled. " + - "Call configureInstrumentation() before importing any AI SDKs.", + "Call configureInstrumentation() before registering instrumentation or wrapping an AI SDK.", ); return; } if ("spanCustomizers" in config) { setSpanCustomizers(config.spanCustomizers); } - this.config = { ...this.config, ...config }; + state.config = { + ...state.config, + ...config, + integrations: { ...state.config.integrations, ...config.integrations }, + }; } - /** - * Enable all configured instrumentation consumers. - * Called automatically when the library is loaded. - */ - enable(): void { - if (this.enabled) { - return; - } - - // If another SDK instance already registered instrumentation consumers, - // skip to avoid duplicate global hook subscriptions. - const sharedState = getSharedState(); - if (sharedState) { - if (sharedState[REGISTRY_STATE_KEY] !== undefined) { - return; - } - sharedState[REGISTRY_STATE_KEY] = this; - } - - this.enabled = true; - - // Read config from environment variables - const envConfig = this.readEnvConfig(); - const finalConfig = { - integrations: { - ...this.getDefaultConfig(), - ...this.config.integrations, - ...envConfig.integrations, - }, + register( + provider: Provider, + subscribe: (integrations: InstrumentationIntegrationsConfig) => void, + ): void { + const state = getRegistryState(); + if (state.registered.has(provider)) return; + const integrations = { + ...state.config.integrations, + ...readDisabledInstrumentationEnvConfig( + iso.getEnv("BRAINTRUST_DISABLE_INSTRUMENTATION"), + ).integrations, }; + if ( + integrations[provider] === false || + (provider === "langchain" && integrations.langgraph === false) + ) + return; - // Enable the configured instrumentation consumers. - registerInstrumentationConsumers(finalConfig); - } - - /** - * Check if instrumentation is enabled. - */ - isEnabled(): boolean { - return this.enabled; + installGlobalHookErrorReporter(); + // Stamp before subscribing, so reentrant setup cannot install twice. + state.registered.add(provider); + subscribe(integrations); } - /** - * Get default configuration (all integrations enabled). - */ - private getDefaultConfig(): Record { - return getDefaultInstrumentationIntegrations(); + isRegistered(provider: Provider): boolean { + return getRegistryState().registered.has(provider); } - /** - * Read configuration from environment variables. - * Supports: BRAINTRUST_DISABLE_INSTRUMENTATION=openai,anthropic,... - */ - private readEnvConfig(): InstrumentationConfig { - return readDisabledInstrumentationEnvConfig( - iso.getEnv("BRAINTRUST_DISABLE_INSTRUMENTATION"), - ); + isEnabled(): boolean { + return getRegistryState().registered.size > 0; } } -/** - * Global instrumentation registry instance. - */ -export const registry = new InstrumentationRegistry(); +export const registry: InstrumentationRegistry = + /* @__PURE__ */ new InstrumentationRegistry(); -/** - * Configure auto-instrumentation. - * - * This must be called before importing any AI SDKs to take effect. - * - * @example - * ```typescript - * import { configureInstrumentation } from 'braintrust/instrumentation'; - * - * // Disable OpenAI instrumentation - * configureInstrumentation({ - * integrations: { openai: false } - * }); - * - * // Now import SDKs - * import OpenAI from 'openai'; - * ``` - * - * Environment variables can also be used: - * ```bash - * # Disable single SDK - * BRAINTRUST_DISABLE_INSTRUMENTATION=openai node app.js - * - * # Disable multiple SDKs - * BRAINTRUST_DISABLE_INSTRUMENTATION=openai,anthropic node app.js - * ``` - */ +/** Configure consumers before importing braintrust/auto or calling a wrapper. */ export function configureInstrumentation(config: InstrumentationConfig): void { registry.configure(config); } diff --git a/js/src/isomorph-base.ts b/js/src/isomorph-base.ts new file mode 100644 index 000000000..953d63890 --- /dev/null +++ b/js/src/isomorph-base.ts @@ -0,0 +1,120 @@ +import type { GitMetadataSettings, RepoInfo } from "./sdk-types"; +import { + newGlobalTracingChannel, + type GlobalHookAsyncLocalStorage, + type GlobalHookHandlers, + type GlobalTracingChannel, + type GlobalTracingChannelCollection, +} from "./global-instrumentation-hooks"; + +export interface CallerLocation { + caller_functionname: string; + caller_filename: string; + caller_lineno: number; +} + +export type IsoAsyncLocalStorage = GlobalHookAsyncLocalStorage; + +class DefaultAsyncLocalStorage implements IsoAsyncLocalStorage { + constructor() {} + + run(_: T | undefined, callback: () => R): R { + return callback(); + } + getStore(): T | undefined { + return undefined; + } +} + +type IsoTracingChannelCollection = GlobalTracingChannelCollection; +export type IsoTracingChannel = GlobalTracingChannel; +export type IsoChannelHandlers = GlobalHookHandlers; + +export interface Common { + buildType: + | "browser" // deprecated, use /workerd or /edge-light entrypoints for edge environments + | "browser-js" // @braintrust/browser package + | "node" + | "edge-light" + | "workerd" + | "unknown"; + + getRepoInfo: ( + settings?: GitMetadataSettings, + ) => Promise; + getPastNAncestors: (n?: number, remote?: string) => Promise; + getEnv: (name: string) => string | undefined; + getBraintrustApiKey: () => Promise; + getCallerLocation: () => CallerLocation | undefined; + newAsyncLocalStorage: () => IsoAsyncLocalStorage; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + newTracingChannel: ( + nameOrChannels: string | IsoTracingChannelCollection, + ) => IsoTracingChannel; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processOn: (event: string, handler: (code: any) => void) => void; + + // hash a string. not guaranteed to be crypto safe. + hash?: (data: string) => string; + + // HMAC-SHA256 for authenticating opaque instrumentation state. + hmacSha256?: (key: string, data: string) => string; + timingSafeEqual?: (left: string, right: string) => boolean; + + // Cross-platform utilities. + basename: (filepath: string) => string; + writeln: (text: string) => void; + + // Filesystem operations (async). + pathJoin?: (...args: string[]) => string; + pathDirname?: (path: string) => string; + mkdir?: ( + path: string, + opts?: { recursive?: boolean }, + ) => Promise; + writeFile?: (filename: string, data: string | Uint8Array) => Promise; + readFile?: (filename: string) => Promise; + readdir?: (path: string) => Promise; + utimes?: (path: string, atime: Date, mtime: Date) => Promise; + unlink?: (path: string) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + stat?: (path: string) => Promise; // type-erased + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statSync?: (path: string) => any; // type-erased + homedir?: () => string; + tmpdir?: () => string; + + // Filesystem operations (sync) - for span cache. + writeFileSync?: (filename: string, data: string) => void; + appendFileSync?: (filename: string, data: string) => void; + readFileSync?: (filename: string, encoding: string) => string; + unlinkSync?: (path: string) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + openFile?: (path: string, flags: string) => Promise; // fs.promises.FileHandle, type-erased + + // zlib (promisified and type-erased). + // eslint-disable-next-line @typescript-eslint/no-explicit-any + gunzip?: (data: any) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + gzip?: (data: any) => Promise; +} + +export function createIsomorph(): Common { + return { + buildType: "unknown", // Will be set by configureBrowser() or configureNode() + getRepoInfo: async (_settings) => undefined, + getPastNAncestors: async () => [], + getEnv: (_name) => undefined, + getBraintrustApiKey: async () => undefined, + getCallerLocation: () => undefined, + newAsyncLocalStorage: () => new DefaultAsyncLocalStorage(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + newTracingChannel: ( + nameOrChannels: string | IsoTracingChannelCollection, + ) => newGlobalTracingChannel(nameOrChannels), + processOn: (_0, _1) => {}, + basename: (filepath: string) => filepath.split(/[\\/]/).pop() || filepath, + // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. + writeln: (text: string) => console.log(text), + }; +} diff --git a/js/src/isomorph.ts b/js/src/isomorph.ts index c06635d0d..87df36cb3 100644 --- a/js/src/isomorph.ts +++ b/js/src/isomorph.ts @@ -1,119 +1,10 @@ -import type { GitMetadataSettings, RepoInfo } from "./sdk-types"; -import { - newGlobalTracingChannel, - type GlobalHookAsyncLocalStorage, - type GlobalHookHandlers, - type GlobalTracingChannel, - type GlobalTracingChannelCollection, -} from "./global-instrumentation-hooks"; - -export interface CallerLocation { - caller_functionname: string; - caller_filename: string; - caller_lineno: number; -} - -export type IsoAsyncLocalStorage = GlobalHookAsyncLocalStorage; - -class DefaultAsyncLocalStorage implements IsoAsyncLocalStorage { - constructor() {} - - run(_: T | undefined, callback: () => R): R { - return callback(); - } - getStore(): T | undefined { - return undefined; - } -} - -type IsoTracingChannelCollection = GlobalTracingChannelCollection; -export type IsoTracingChannel = GlobalTracingChannel; -export type IsoChannelHandlers = GlobalHookHandlers; - -interface Common { - buildType: - | "browser" // deprecated, use /workerd or /edge-light entrypoints for edge environments - | "browser-js" // @braintrust/browser package - | "node" - | "edge-light" - | "workerd" - | "unknown"; - - getRepoInfo: ( - settings?: GitMetadataSettings, - ) => Promise; - getPastNAncestors: (n?: number, remote?: string) => Promise; - getEnv: (name: string) => string | undefined; - getBraintrustApiKey: () => Promise; - getCallerLocation: () => CallerLocation | undefined; - newAsyncLocalStorage: () => IsoAsyncLocalStorage; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - newTracingChannel: ( - nameOrChannels: string | IsoTracingChannelCollection, - ) => IsoTracingChannel; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processOn: (event: string, handler: (code: any) => void) => void; - - // hash a string. not guaranteed to be crypto safe. - hash?: (data: string) => string; - - // HMAC-SHA256 for authenticating opaque instrumentation state. - hmacSha256?: (key: string, data: string) => string; - timingSafeEqual?: (left: string, right: string) => boolean; - - // Cross-platform utilities. - basename: (filepath: string) => string; - writeln: (text: string) => void; - - // Filesystem operations (async). - pathJoin?: (...args: string[]) => string; - pathDirname?: (path: string) => string; - mkdir?: ( - path: string, - opts?: { recursive?: boolean }, - ) => Promise; - writeFile?: (filename: string, data: string | Uint8Array) => Promise; - readFile?: (filename: string) => Promise; - readdir?: (path: string) => Promise; - utimes?: (path: string, atime: Date, mtime: Date) => Promise; - unlink?: (path: string) => Promise; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - stat?: (path: string) => Promise; // type-erased - // eslint-disable-next-line @typescript-eslint/no-explicit-any - statSync?: (path: string) => any; // type-erased - homedir?: () => string; - tmpdir?: () => string; - - // Filesystem operations (sync) - for span cache. - writeFileSync?: (filename: string, data: string) => void; - appendFileSync?: (filename: string, data: string) => void; - readFileSync?: (filename: string, encoding: string) => string; - unlinkSync?: (path: string) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - openFile?: (path: string, flags: string) => Promise; // fs.promises.FileHandle, type-erased - - // zlib (promisified and type-erased). - // eslint-disable-next-line @typescript-eslint/no-explicit-any - gunzip?: (data: any) => Promise; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - gzip?: (data: any) => Promise; -} - -const iso: Common = { - buildType: "unknown", // Will be set by configureBrowser() or configureNode() - getRepoInfo: async (_settings) => undefined, - getPastNAncestors: async () => [], - getEnv: (_name) => undefined, - getBraintrustApiKey: async () => undefined, - getCallerLocation: () => undefined, - newAsyncLocalStorage: () => new DefaultAsyncLocalStorage(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - newTracingChannel: ( - nameOrChannels: string | IsoTracingChannelCollection, - ) => newGlobalTracingChannel(nameOrChannels), - processOn: (_0, _1) => {}, - basename: (filepath: string) => filepath.split(/[\\/]/).pop() || filepath, - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - writeln: (text: string) => console.log(text), -}; +import type { Common } from "./isomorph-base"; +export type { + CallerLocation, + IsoAsyncLocalStorage, + IsoTracingChannel, + IsoChannelHandlers, +} from "./isomorph-base"; +import { createNodeIsomorph } from "./node/platform"; +const iso: Common = /* @__PURE__ */ createNodeIsomorph(); export default iso; diff --git a/js/src/logger-caching.test.ts b/js/src/logger-caching.test.ts index 7831cc060..66781e984 100644 --- a/js/src/logger-caching.test.ts +++ b/js/src/logger-caching.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test, vi } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vite-plus/test"; import { BraintrustState, initLogger, @@ -6,9 +6,9 @@ import { startSpan, updateSpan, _internalResumeSpan, -} from "./logger"; +} from "./logger/core"; import { configureNode } from "./node/config"; -import iso from "./isomorph"; +import iso from "#braintrust-platform"; import { SpanComponentsV4 } from "../util/span_identifier_v4"; configureNode(); diff --git a/js/src/logger-json-attachment.test.ts b/js/src/logger-json-attachment.test.ts index 48fbedb5a..1d392723f 100644 --- a/js/src/logger-json-attachment.test.ts +++ b/js/src/logger-json-attachment.test.ts @@ -1,9 +1,6 @@ -import { - JSONAttachment, - _exportsForTestingOnly, - BaseAttachment, -} from "./logger"; -import { describe, expect, it } from "vitest"; +import { JSONAttachment, BaseAttachment } from "./logger/core"; +import { _exportsForTestingOnly } from "./logger/testing"; +import { describe, expect, it } from "vite-plus/test"; const { extractAttachments } = _exportsForTestingOnly; diff --git a/js/src/logger-misc.test.ts b/js/src/logger-misc.test.ts index 730bacb68..ce5ff9ce2 100644 --- a/js/src/logger-misc.test.ts +++ b/js/src/logger-misc.test.ts @@ -4,18 +4,25 @@ */ /* eslint-disable @typescript-eslint/consistent-type-assertions */ -import { vi, expect, test, describe, beforeEach, afterEach } from "vitest"; import { - _exportsForTestingOnly, + vi, + expect, + test, + describe, + beforeEach, + afterEach, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "./logger/testing"; +import { BaseAttachment, Attachment, ExternalAttachment, - init, initLogger, NOOP_SPAN, permalink, BraintrustState, -} from "./logger"; +} from "./logger/core"; +import { init } from "./logger/experiments"; import { SpanObjectTypeV3 } from "../util/index"; import { LazyValue } from "./util"; import { BackgroundLogEvent, IS_MERGE_FIELD } from "../util/index"; diff --git a/js/src/logger.test.ts b/js/src/logger.test.ts index 3bc66d532..e3873a207 100644 --- a/js/src/logger.test.ts +++ b/js/src/logger.test.ts @@ -1,3 +1,4 @@ +import { getLoaderCaches } from "./logger/prompts"; /* eslint-disable @typescript-eslint/consistent-type-assertions */ import { @@ -8,20 +9,14 @@ import { describe, beforeEach, afterEach, -} from "vitest"; +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "./logger/testing"; import { - _exportsForTestingOnly, _internalStartSpan, - init, - initDataset, initLogger, extractTraceContextFromHeaders, - Prompt, - RemoteEvalParameters, BraintrustState, FailedHTTPResponse, - loadPrompt, - loadParameters, wrapTraced, traced, currentSpan, @@ -30,11 +25,18 @@ import { updateSpan, Attachment, deepCopyEvent, - ReadonlyExperiment, - renderMessageImpl, flush, type InitLoggerOptions, -} from "./logger"; +} from "./logger/core"; +import { init, ReadonlyExperiment } from "./logger/experiments"; +import { initDataset } from "./logger/datasets"; +import { + Prompt, + RemoteEvalParameters, + loadPrompt, + loadParameters, + renderMessageImpl, +} from "./logger/prompts"; import { configureNode } from "./node/config"; import { type GitMetadataSettingsType as GitMetadataSettings } from "./generated_types"; @@ -2380,8 +2382,14 @@ describe("prompt and parameters loaders", () => { ) .mockResolvedValueOnce(jsonResponse({ objects: [promptRow] })) .mockResolvedValueOnce(serverErrorResponse()); - const globalCacheGet = vi.spyOn(state.promptCache, "get"); - const globalCacheSet = vi.spyOn(state.promptCache, "set"); + const globalCacheGet = vi.spyOn( + getLoaderCaches(state).promptCache, + "get", + ); + const globalCacheSet = vi.spyOn( + getLoaderCaches(state).promptCache, + "set", + ); const options = { projectName: "test-project", @@ -2784,7 +2792,7 @@ describe("prompt and parameters loaders", () => { logWarnings: false, }); const diskCacheSet = vi.spyOn(onlineDiskCache, "set"); - onlineState.promptCache = new PromptCache({ + getLoaderCaches(onlineState).promptCache = new PromptCache({ diskCache: onlineDiskCache, }); await onlineState.login({}); @@ -2813,7 +2821,7 @@ describe("prompt and parameters loaders", () => { fetch: offlineFetch as unknown as typeof globalThis.fetch, noExitFlush: true, }); - offlineState.promptCache = new PromptCache({ + getLoaderCaches(offlineState).promptCache = new PromptCache({ diskCache: new DiskCache({ cacheDir, logWarnings: false }), }); @@ -2838,7 +2846,7 @@ describe("prompt and parameters loaders", () => { fetch: rejectedFetch as unknown as typeof globalThis.fetch, noExitFlush: true, }); - rejectedState.promptCache = new PromptCache({ + getLoaderCaches(rejectedState).promptCache = new PromptCache({ diskCache: new DiskCache({ cacheDir, logWarnings: false }), }); @@ -2879,7 +2887,7 @@ describe("prompt and parameters loaders", () => { fetch: fetchMock as unknown as typeof globalThis.fetch, noExitFlush: true, }); - switchingState.promptCache = new PromptCache({ + getLoaderCaches(switchingState).promptCache = new PromptCache({ memoryCache: new LRUCache({ max: 10 }), }); @@ -2904,10 +2912,10 @@ describe("prompt and parameters loaders", () => { }); test("loadPrompt does not read legacy unnamespaced cache entries", async () => { - state.promptCache = new PromptCache({ + getLoaderCaches(state).promptCache = new PromptCache({ memoryCache: new LRUCache({ max: 10 }), }); - await state.promptCache.set( + await getLoaderCaches(state).promptCache.set( { projectName: "test-project", slug: "saved-prompt", @@ -2996,7 +3004,7 @@ describe("prompt and parameters loaders", () => { }); test("loadParameters uses request-local credentials and isolated caches", async () => { - state.parametersCache = new ParametersCache({ + getLoaderCaches(state).parametersCache = new ParametersCache({ memoryCache: new LRUCache({ max: 10, }), diff --git a/js/src/logger.ts b/js/src/logger.ts index a49594be1..4ae948ee8 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -1,10138 +1,149 @@ -/// - -import { v4 as uuidv4 } from "uuid"; - -import { Queue, DEFAULT_QUEUE_SIZE } from "./queue"; -import { - debugLogger, - type DebugLogLevel, - type DebugLogLevelOption, - getEnvDebugLogLevel, - normalizeDebugLogLevelOption, - resetDebugLoggerForTests, - setDebugLogStateResolver, - setGlobalDebugLogLevel, -} from "./debug-logger"; -import { IDGenerator, getIdGenerator, resolveUseLegacyUuidIds } from "./id-gen"; -import { - BAGGAGE_HEADER, - BRAINTRUST_PARENT_KEY, - PropagatedState, - TRACEPARENT_HEADER, - TraceContextCarrier, - TraceContextHeaders, - TRACESTATE_HEADER, - formatTraceparent, - getHeader, - mergeBaggage, - parseBaggage, - parseTraceparent, -} from "./propagation"; -import { - _urljoin, - AnyDatasetRecord, - AUDIT_METADATA_FIELD, - AUDIT_SOURCE_FIELD, - BackgroundLogEvent, - batchItems, - constructJsonArray, - DatasetRecord, - ensureNewDatasetRecord, - ExperimentEvent, - ExperimentLogFullArgs, - ExperimentLogPartialArgs, - IdField, - IS_MERGE_FIELD, - LogFeedbackFullArgs, - mergeDicts, - mergeGitMetadataSettings, - mergeRowBatch, - OBJECT_DELETE_FIELD, - OBJECT_ID_KEYS, - SanitizedExperimentLogPartialArgs, - SpanComponentsV3, - SpanComponentsV4, - SpanComponentsV4Data, - SpanObjectTypeV3, - spanObjectTypeV3ToString, - SpanType, - SpanTypeAttribute, - TRANSACTION_ID_FIELD, - TransactionId, - VALID_SOURCES, - isArray, - isObject, - getObjValueByPath, -} from "./util"; -import { - AttachmentReference as attachmentReferenceSchema, - BraintrustAttachmentReference as BraintrustAttachmentReferenceSchema, - BraintrustModelParams as braintrustModelParamsSchema, - ChatCompletionTool as chatCompletionToolSchema, - ExternalAttachmentReference as ExternalAttachmentReferenceSchema, - ResponseFormatJsonSchema as responseFormatJsonSchemaSchema, - AttachmentStatus as attachmentStatusSchema, - GitMetadataSettings as gitMetadataSettingsSchema, - DatasetSnapshot as datasetSnapshotSchema, - PromptData as promptDataSchema, - Prompt as promptSchema, -} from "./sdk-schemas"; -import type { - AttachmentReference, - BraintrustAttachmentReference, - ChatCompletionTool, - ExternalAttachmentReference, - ModelParams, - AttachmentStatus, - GitMetadataSettings, - ChatCompletionMessageParam as Message, - ChatCompletionOpenAIMessageParam as OpenAIMessage, - DatasetSnapshot, - PromptData, - FunctionTypeEnum, - PromptSessionEvent, - RepoInfo, - ObjectReference, - PromptBlockData, -} from "./sdk-types"; - -const BRAINTRUST_ATTACHMENT = - BraintrustAttachmentReferenceSchema.shape.type.value; -const EXTERNAL_ATTACHMENT = ExternalAttachmentReferenceSchema.shape.type.value; -export const LOGS3_OVERFLOW_REFERENCE_TYPE = "logs3_overflow"; -const BRAINTRUST_PARAMS = Object.keys(braintrustModelParamsSchema.shape); -const RESET_CONTEXT_MANAGER_STATE = Symbol.for( - "braintrust.resetContextManagerState", -); -// 6 MB for the AWS lambda gateway (from our own testing). -export const DEFAULT_MAX_REQUEST_SIZE = 6 * 1024 * 1024; - -export type { DatasetSnapshot }; - -const datasetSnapshotRegisterResponseSchema = z.object({ - dataset_snapshot: datasetSnapshotSchema, - found_existing: z.boolean().optional(), -}); - -const datasetObjectInfoSchema = z.object({ - object_id: z.string(), - object_name: z.string(), - parent_cols: z.object({ - project: z.object({ - id: z.string(), - name: z.string(), - }), - }), -}); - -const datasetRestorePreviewResultSchema = z.object({ - rows_to_restore: z.number(), - rows_to_delete: z.number(), -}); -export type DatasetRestorePreviewResult = { - rows_to_restore: number; - rows_to_delete: number; -}; - -const datasetRestoreResultSchema = z.object({ - xact_id: z.string().nullable(), - rows_restored: z.number(), - rows_deleted: z.number(), -}); -export type DatasetRestoreResult = { - xact_id: string | null; - rows_restored: number; - rows_deleted: number; -}; - -const parametersRowSchema = z.object({ - id: z.string().uuid(), - _xact_id: z.string(), - project_id: z.string().uuid(), - name: z.string(), - slug: z.string(), - description: z.union([z.string(), z.null()]).optional(), - function_type: z.literal("parameters"), - function_data: z.object({ - type: z.literal("parameters"), - data: z.record(z.unknown()).optional(), - __schema: z.record(z.unknown()), - }), - metadata: z - .union([z.object({}).partial().passthrough(), z.null()]) - .optional(), -}); -type ParametersRow = { - id: string; - _xact_id: string; - project_id: string; - name: string; - slug: string; - description?: string | null; - function_type: "parameters"; - function_data: { - type: "parameters"; - data?: Record; - __schema: Record; - }; - metadata?: Record | null; -}; - -import { waitUntil } from "@vercel/functions"; -import { - parseTemplateFormat, - renderTemplateContent, -} from "./template/renderer"; -import type { TemplateFormat } from "./template/registry"; - -import { z, ZodError } from "zod/v3"; -import { - BraintrustStream, - createFinalValuePassThroughStream, - devNullWritableStream, -} from "./functions/stream"; -import iso, { IsoAsyncLocalStorage } from "./isomorph"; -import { createCacheLayers } from "./prompt-cache/cache-config"; -import { - PromptCache, - type PromptDiskCacheEntry, - type PromptMemoryCacheEntry, -} from "./prompt-cache/prompt-cache"; -import { - ParametersCache, - type ParametersDiskCacheEntry, - type ParametersMemoryCacheEntry, -} from "./prompt-cache/parameters-cache"; -import { LRUCache } from "./lru-cache"; -import { - addAzureBlobHeaders, - getCurrentUnixTimestamp, - GLOBAL_PROJECT, - isEmpty, - LazyValue, - SyncLazyValue, - runCatchFinally, -} from "./util"; -import { lintTemplate as _lintMustacheTemplate } from "./template/mustache-utils"; -import { prettifyXact } from "../util/index"; -import { SpanCache, CachedSpan } from "./span-cache"; -import type { EvalParameters, InferParameters } from "./eval-parameters"; -import { - detectSpanOriginEnvironment, - getSpanInstrumentationName, - INSTRUMENTATION_NAMES, - mergeSpanOriginContext, - type SpanOriginEnvironment, -} from "./span-origin"; -import { - createMaskingCustomizer, - customizeSpanExport, -} from "./span-customizer"; -import type { SpanCustomizer, SpanExportData } from "./instrumentation/config"; - -// Manual type definition for inline attachments (not in generated_types) -const InlineAttachmentReferenceSchema = z.object({ - type: z.literal("inline_attachment"), - src: z.string().min(1), - content_type: z.string().optional(), - filename: z.string().optional(), -}); - -// Context management interfaces -export interface ContextParentSpanIds { - rootSpanId: string; - spanParents: string[]; -} - -export class LoginInvalidOrgError extends Error { - constructor(public message: string) { - super(message); - } -} - -export type SetCurrentArg = { setCurrent?: boolean }; - -type StartSpanEventArgs = ExperimentLogPartialArgs & Partial; - -const INITIAL_SPAN_WRITE_AS_MERGE = Symbol( - "braintrust.initial-span-write-as-merge", -); -const RESUME_SPAN_WITHOUT_INITIAL_WRITE = Symbol( - "braintrust.resume-span-without-initial-write", -); -const INTERNAL_SPAN_CONTEXT = Symbol("braintrust.internal-span-context"); - -type InitialSpanWriteAsMergeArg = { - readonly [INITIAL_SPAN_WRITE_AS_MERGE]?: true; - readonly [RESUME_SPAN_WITHOUT_INITIAL_WRITE]?: true; -}; - -type InternalSpanContextArg = { - readonly [INTERNAL_SPAN_CONTEXT]?: Record; -}; - -export type StartSpanArgs = { - name?: string; - type?: SpanType; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - spanAttributes?: Record; - startTime?: number; - /** - * An opaque W3C trace context returned by - * {@link extractTraceContextFromHeaders}. - */ - parent?: PropagationContext; - event?: StartSpanEventArgs; - propagatedEvent?: StartSpanEventArgs; - spanId?: string; - parentSpanIds?: ParentSpanIds | MultiParentSpanIds; -}; - -type InternalStartSpanArgs = Omit & { - parent?: string | PropagationContext; -}; - -export type EndSpanArgs = { - endTime?: number; -}; - -export interface Exportable { - /** - * Return a serialized representation of the object that can be used to start subspans in other places. See {@link Span.traced} for more details. - */ - export(): Promise; -} - -/** - * A Span encapsulates logged data and metrics for a unit of work. This interface is shared by all span implementations. - * - * We suggest using one of the various `traced` methods, instead of creating Spans directly. See {@link Span.traced} for full details. - */ -export interface Span extends Exportable { - /** - * Row ID of the span. - */ - id: string; - - /** - * Span ID of the span. - */ - spanId: string; - - /** - * Root span ID of the span. - */ - rootSpanId: string; - - /** - * Parent span IDs of the span. - */ - spanParents: string[]; - - getParentInfo(): - | { - objectType: SpanObjectTypeV3; - objectId: LazyValue; - computeObjectMetadataArgs: Record | undefined; - } - | undefined; - - /** - * Incrementally update the current span with new data. The event will be batched and uploaded behind the scenes. - * - * @param event: Data to be logged. See {@link Experiment.log} for full details. - */ - log(event: ExperimentLogPartialArgs): void; - - /** - * Add feedback to the current span. Unlike `Experiment.logFeedback` and `Logger.logFeedback`, this method does not accept an id parameter, because it logs feedback to the current span. - * - * @param event: Data to be logged. See {@link Experiment.logFeedback} for full details. - */ - logFeedback(event: Omit): void; - - /** - * Create a new span and run the provided callback. This is useful if you want to log more detailed trace information beyond the scope of a single log event. Data logged over several calls to `Span.log` will be merged into one logical row. - * - * Spans created within `traced` are ended automatically. By default, the span is marked as current, so they can be accessed using `braintrust.currentSpan`. - * - * @param callback The function to be run under the span context. - * @param args.name Optional name of the span. If not provided, a name will be inferred from the call stack. - * @param args.type Optional type of the span. If not provided, the type will be unset. - * @param args.span_attributes Optional additional attributes to attach to the span, such as a type name. - * @param args.start_time Optional start time of the span, as a timestamp in seconds. - * @param args.setCurrent If true (the default), the span will be marked as the currently-active span for the duration of the callback. - * @param args.parent Optional parent info string for the span. The string can be generated from `[Span,Experiment,Logger].export`. If not provided, the current span will be used (depending on context). This is useful for adding spans to an existing trace. - * @param args.event Data to be logged. See {@link Experiment.log} for full details. - * @returns The result of running `callback`. - */ - traced( - callback: (span: Span) => R, - args?: StartSpanArgs & SetCurrentArg, - ): R; - - /** - * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations - * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", - * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead. - * - * See {@link Span.traced} for full details. - * - * @returns The newly-created `Span` - */ - startSpan(args?: StartSpanArgs): Span; - - /** - * Log an end time to the span (defaults to the current time). Returns the logged time. - * - * Will be invoked automatically if the span is constructed with `traced`. - * - * @param args.endTime Optional end time of the span, as a timestamp in seconds. - * @returns The end time logged to the span metrics. - */ - end(args?: EndSpanArgs): number; - - /** - * Serialize the identifiers of this span. The return value can be used to - * identify this span when starting a subspan elsewhere, such as another - * process or service, without needing to access this `Span` object. See the - * parameters of {@link Span.startSpan} for usage details. - * - * Callers should treat the return value as opaque. The serialization format - * may change from time to time. For cross-service propagation, prefer - * {@link Span.inject} and {@link extractTraceContextFromHeaders}. - * - * @returns Serialized representation of this span's identifiers. - */ - export(): Promise; - - /** - * Inject W3C trace-context headers (`traceparent` and `baggage`) for this span - * into a carrier, for distributed tracing across service boundaries. - * - * Adds `traceparent` (trace identity) and, when this span's Braintrust parent - * is known, a `baggage` entry `braintrust.parent=` (merged with any - * pre-existing baggage). Propagation is best-effort and never throws; if the - * span's ids are not W3C-shaped hex (e.g. legacy UUID mode), `traceparent` is - * omitted. - * - * @param carrier Optional existing carrier (e.g. outbound HTTP headers) to - * mutate and return. Supports plain objects, Web `Headers`, Node-style - * `setHeader` carriers, framework `header` carriers, and mutable - * `HeadersInit` tuple arrays. A new object is created if not provided. - * @returns The carrier with propagation headers injected. - */ - inject(): Record; - inject(carrier: T): T; - - /** - * Format a permalink to the Braintrust application for viewing this span. - * - * Links can be generated at any time, but they will only become viewable - * after the span and its root have been flushed to the server and ingested. - * - * This function can block resolving data with the server. For production - * applications it's preferable to call {@link Span.link} instead. - * - * @returns A promise which resolves to a permalink to the span. - */ - permalink(): Promise; - - /** - * Format a link to the Braintrust application for viewing this span. - * - * Links can be generated at any time, but they will only become viewable - * after the span and its root have been flushed to the server and ingested. - * - * There are some conditions when a Span doesn't have enough information - * to return a stable link (e.g. during an unresolved experiment). In this case - * or if there's an error generating link, we'll return a placeholder link. - * - * @returns A link to the span. - */ - link(): string; - - /** - * Flush any pending rows to the server. - */ - flush(): Promise; - - /** - * Set the span's name, type, or other attributes after it's created. - */ - setAttributes(args: Omit): void; - - /** - * Start a span with a specific id and parent span ids. - */ - startSpanWithParents( - spanId: string, - spanParents: string[], - args?: StartSpanArgs, - ): Span; - - /* - * Gets the span's `state` value, which is usually the global logging state (this is - * for very advanced purposes only) - */ - state(): BraintrustState; - - // For type identification. - kind: "span"; -} - -/** - * The AsyncLocalStorage contract used by context-manager integrations. - * - * The stored value is intentionally opaque (`unknown`) because the concrete type - * depends on the active context manager: - * - Default (`BraintrustContextManager`): stores a `Span` - * - OTEL compat (`OtelContextManager`): stores an OTEL `Context` object - * - * The global hook's `bindStore` transform (via `wrapSpanForStore`) produces the - * correct value type for whichever mode is active. - */ -export type CurrentSpanStore = IsoAsyncLocalStorage; - -export abstract class ContextManager { - abstract getParentSpanIds(): ContextParentSpanIds | undefined; - abstract runInContext(span: Span, callback: () => R): R; - abstract getCurrentSpan(): Span | undefined; - - /** - * Return the store used by instrumentation hooks to propagate the current - * span, when the context manager supports direct store binding. - */ - getCurrentSpanStore(): CurrentSpanStore | undefined { - return undefined; - } - - /** - * Returns the value to store in the ALS bound to a global hook's start event. - * In default mode this is the Span itself; in OTEL mode it is the OTEL Context - * containing the span so that OTEL's own ALS stores a proper Context object. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - wrapSpanForStore(span: Span): unknown { - return span; - } -} - -class BraintrustContextManager extends ContextManager { - private _currentSpan: IsoAsyncLocalStorage; - - constructor() { - super(); - this._currentSpan = iso.newAsyncLocalStorage(); - } - - getCurrentSpanStore(): CurrentSpanStore { - return this._currentSpan; - } - - getParentSpanIds(): ContextParentSpanIds | undefined { - const currentSpan = this._currentSpan.getStore(); - if (!currentSpan) { - return undefined; - } - - return { - rootSpanId: currentSpan.rootSpanId, - spanParents: [currentSpan.spanId], - }; - } - - runInContext(span: Span, callback: () => R): R { - return this._currentSpan.run(span, callback); - } - - getCurrentSpan(): Span | undefined { - return this._currentSpan.getStore(); - } -} - -declare global { - var BRAINTRUST_CONTEXT_MANAGER: (new () => ContextManager) | undefined; -} - -type SpanComponent = typeof SpanComponentsV3 | typeof SpanComponentsV4; - -// Return the active span-component exporter class. -// -// The export version is coupled to the active ID format: hex IDs (the default) -// serialize as V4, legacy UUID IDs serialize as V3. These must move together -- -// serializing hex IDs via V3 would lose the compact encoding and risk -// corrupting hex values that happen to parse as UUIDs. An explicit -function getSpanComponentsClass(): SpanComponent { - return resolveUseLegacyUuidIds() ? SpanComponentsV3 : SpanComponentsV4; -} - -export function getContextManager(): ContextManager { - return globalThis.BRAINTRUST_CONTEXT_MANAGER - ? new globalThis.BRAINTRUST_CONTEXT_MANAGER() - : new BraintrustContextManager(); -} - -/** - * Configure the context manager used by Braintrust. - * - * This extension point is intended for runtime integrations such as - * `@braintrust/otel` and `@braintrust/browser`. Passing `undefined` restores - * Braintrust's default context manager. - */ -export function configureContextManager( - contextManager: (new () => ContextManager) | undefined, -): void { - globalThis.BRAINTRUST_CONTEXT_MANAGER = contextManager; - _globalState?.[RESET_CONTEXT_MANAGER_STATE](); -} - -/** - * A fake implementation of the Span API which does nothing. This can be used as the default span. - */ -export class NoopSpan implements Span { - public id: string; - public spanId: string; - public rootSpanId: string; - public spanParents: string[]; - - public kind: "span" = "span" as const; - - constructor() { - this.id = ""; - this.spanId = ""; - this.rootSpanId = ""; - this.spanParents = []; - } - - public log(_: ExperimentLogPartialArgs) {} - - public logFeedback(_event: Omit) {} - - public traced( - callback: (span: Span) => R, - _1?: StartSpanArgs & SetCurrentArg, - ): R { - return callback(this); - } - - public getParentInfo() { - return undefined; - } - - public startSpan(_1?: StartSpanArgs) { - return this; - } - - public end(args?: EndSpanArgs): number { - return args?.endTime ?? getCurrentUnixTimestamp(); - } - - public async export(): Promise { - return ""; - } - - public inject(): Record; - public inject(carrier: T): T; - public inject( - carrier?: T, - ): T | Record { - return carrier ?? {}; - } - - public async permalink(): Promise { - return NOOP_SPAN_PERMALINK; - } - - public link(): string { - return NOOP_SPAN_PERMALINK; - } - - public async flush(): Promise {} - - public setAttributes(_args: Omit) {} - - public startSpanWithParents( - _spanId: string, - _spanParents: string[], - _args?: StartSpanArgs, - ): Span { - return this; - } - - public state() { - return _internalGetGlobalState(); - } - - // Custom inspect for Node.js console.log - [Symbol.for("nodejs.util.inspect.custom")](): string { - return `NoopSpan { - kind: '${this.kind}', - id: '${this.id}', - spanId: '${this.spanId}', - rootSpanId: '${this.rootSpanId}', - spanParents: ${JSON.stringify(this.spanParents)} -}`; - } - - // Custom toString - toString(): string { - return `NoopSpan(id=${this.id}, spanId=${this.spanId})`; - } -} - -export const NOOP_SPAN = new NoopSpan(); -export const NOOP_SPAN_PERMALINK = "https://braintrust.dev/noop-span"; - -// In certain situations (e.g. the cli), we want separately-compiled modules to -// use the same state as the toplevel module. Use a symbol on `globalThis. -declare global { - interface Global { - [key: symbol]: any; - } -} - -const loginSchema = z.strictObject({ - appUrl: z.string(), - appPublicUrl: z.string(), - orgName: z.string(), - apiUrl: z.string(), - proxyUrl: z.string(), - loginToken: z.string(), - orgId: z.string().nullish(), - gitMetadataSettings: gitMetadataSettingsSchema.nullish(), - debugLogLevel: z.enum(["error", "warn", "info", "debug"]).optional(), - // Distinguishes explicit false from unset so env fallback stays disabled after deserialization. - debugLogLevelDisabled: z.boolean().optional(), -}); - -export type SerializedBraintrustState = { - appUrl: string; - appPublicUrl: string; - orgName: string; - apiUrl: string; - proxyUrl: string; - loginToken: string; - orgId?: string | null; - gitMetadataSettings?: GitMetadataSettings | null; - debugLogLevel?: "error" | "warn" | "info" | "debug"; - debugLogLevelDisabled?: boolean; -}; - -let stateNonce = 0; - -const V1_PROXY_SUFFIX = "/v1/proxy"; -const LOADER_LOGIN_CACHE_MAX = 16; -const LOGIN_TIMEOUT_MS = 30_000; -const PROJECT_LOOKUP_TIMEOUT_MS = 30_000; -const PROJECT_CACHE_TTL_MS = 15 * 60 * 1000; - -type ResolvedLoaderLoginOptions = Required< - Pick -> & - Pick & { - forceLogin?: boolean; - credentialCacheNamespace: string; - existingState?: BraintrustState; - }; - -type LoaderCacheViews = { - promptCache: PromptCache; - parametersCache: ParametersCache; -}; - -type LoaderRequestState = Pick; - -// proxyUrl may point at the universal proxy (`{apiUrl}/v1/proxy`) for -// EU/self-hosted orgs, but proxyConn only targets Braintrust API endpoints -// (e.g. function/invoke, function/sandbox-list) served at the API host root. -// Drop the suffix so these requests resolve on all data planes. -function normalizeProxyConnUrl(proxyUrl: string): string { - return proxyUrl.endsWith(V1_PROXY_SUFFIX) - ? proxyUrl.slice(0, proxyUrl.length - V1_PROXY_SUFFIX.length) - : proxyUrl; -} - -/** @internal */ -export const CAPTURE_ATTACHMENTS = Symbol.for("braintrust.captureAttachments"); - -function captureAttachmentsFromEnv(): boolean { - return ["1", "true"].includes( - iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS")?.trim().toLowerCase() ?? "", - ); -} - -/** - * @internal Resolve automatic attachment capture for a logger or span. Only plain - * fields are read, so parents and global state from other SDK bundles also work. - */ -export function _internalCaptureAttachmentsEnabled(parent?: object): boolean { - // Global state is unset until a platform entrypoint configures the SDK. - const owner = parent ?? (_globalState ? getSpanParentObject() : undefined); - const policy = owner && Reflect.get(owner, CAPTURE_ATTACHMENTS); - return typeof policy === "boolean" - ? policy - : (_globalState?.captureAttachments ?? captureAttachmentsFromEnv()); -} - -export class BraintrustState { - public id: string; - public currentExperiment: Experiment | undefined; - public currentLogger: Logger | undefined; - public currentParent: IsoAsyncLocalStorage; - public currentSpan: IsoAsyncLocalStorage; - // Any time we re-log in, we directly update the apiConn inside the logger. - // This is preferable to replacing the whole logger, which would create the - // possibility of multiple loggers floating around, which may not log in a - // deterministic order. - private _bgLogger: SyncLazyValue; - private _overrideBgLogger: BackgroundLogger | null = null; - - public appUrl: string | null = null; - public appPublicUrl: string | null = null; - public loginToken: string | null = null; - public orgId: string | null = null; - public orgName: string | null = null; - public apiUrl: string | null = null; - public proxyUrl: string | null = null; - public loggedIn: boolean = false; - public gitMetadataSettings?: GitMetadataSettings; - /** @internal Default for instrumentation without a logger-specific policy. */ - public captureAttachments?: boolean; - public debugLogLevel?: DebugLogLevel; - private debugLogLevelConfigured = false; - - public fetch: typeof globalThis.fetch = globalThis.fetch; - private _appConn: HTTPConnection | null = null; - private _apiConn: HTTPConnection | null = null; - private _proxyConn: HTTPConnection | null = null; - - public promptCache: PromptCache; - public parametersCache: ParametersCache; - public spanCache: SpanCache; - /** @internal */ - public readonly projectMetadataCache = new LRUCache< - string, - { promise: Promise; expiresAt: number } - >({ max: 1000 }); - private _idGenerator: IDGenerator | null = null; - private _contextManager: ContextManager | null = null; - private _otelFlushCallback: (() => Promise) | null = null; - public spanOriginEnvironment: SpanOriginEnvironment | undefined; - private traceContextSigningSecret: string | undefined; - private loaderLoginCache = new WeakMap< - typeof globalThis.fetch, - LRUCache> - >(); - - private readonly loginParams: LoginOptions; - private activeLoginOrgNameSelector: string | undefined; - private loginGeneration = 0; - private loginQueueTail: Promise | undefined; - private pendingLogins = new WeakMap< - typeof globalThis.fetch, - Map> - >(); - - constructor(loginParams: LoginOptions) { - this.loginParams = { ...loginParams }; - this.activeLoginOrgNameSelector = - loginParams.orgName ?? iso.getEnv("BRAINTRUST_ORG_NAME"); - this.id = `${new Date().toLocaleString()}-${stateNonce++}`; // This is for debugging. uuidv4() breaks on platforms like Cloudflare. - this.currentExperiment = undefined; - this.currentLogger = undefined; - this.currentParent = iso.newAsyncLocalStorage< - string | PropagationContext - >(); - this.currentSpan = iso.newAsyncLocalStorage(); - - if (loginParams.fetch) { - this.fetch = loginParams.fetch; - } - - const defaultGetLogConn = async () => { - await this.login({}); - return this.apiConn(); - }; - this._bgLogger = new SyncLazyValue( - () => - new HTTPBackgroundLogger(new LazyValue(defaultGetLogConn), loginParams), - ); - - if (loginParams.debugLogLevel !== undefined) { - this.debugLogLevelConfigured = true; - this.debugLogLevel = normalizeDebugLogLevelOption( - loginParams.debugLogLevel, - ); - setGlobalDebugLogLevel(this.debugLogLevel ?? false); - } else { - this.debugLogLevel = getEnvDebugLogLevel(); - setGlobalDebugLogLevel(undefined); - } - - this.resetLoginInfo(); - - const { memoryCache, diskCache } = createCacheLayers< - PromptMemoryCacheEntry, - PromptDiskCacheEntry - >({ - memoryMaxEnvVar: "BRAINTRUST_PROMPT_CACHE_MEMORY_MAX", - diskCacheDirEnvVar: "BRAINTRUST_PROMPT_CACHE_DIR", - diskMaxEnvVar: "BRAINTRUST_PROMPT_CACHE_DISK_MAX", - getDefaultDiskCacheDir: () => - `${iso.getEnv("HOME") ?? iso.homedir!()}/.braintrust/prompt_cache`, - }); - this.promptCache = new PromptCache({ memoryCache, diskCache }); - - const { - memoryCache: parametersMemoryCache, - diskCache: parametersDiskCache, - } = createCacheLayers( - { - memoryMaxEnvVar: "BRAINTRUST_PARAMETERS_CACHE_MEMORY_MAX", - diskCacheDirEnvVar: "BRAINTRUST_PARAMETERS_CACHE_DIR", - diskMaxEnvVar: "BRAINTRUST_PARAMETERS_CACHE_DISK_MAX", - getDefaultDiskCacheDir: () => - `${iso.getEnv("HOME") ?? iso.homedir!()}/.braintrust/parameters_cache`, - }, - ); - this.parametersCache = new ParametersCache({ - memoryCache: parametersMemoryCache, - diskCache: parametersDiskCache, - }); - - this.spanCache = new SpanCache({ disabled: loginParams.disableSpanCache }); - this.spanOriginEnvironment = detectSpanOriginEnvironment(); - this._internalSetTraceContextSigningSecret(loginParams.apiKey); - } - - /** @internal */ - public _internalSetTraceContextSigningSecret( - secret: string | undefined, - ): void { - const normalizedSecret = secret?.trim(); - if (normalizedSecret) { - this.traceContextSigningSecret = normalizedSecret; - } - } - - /** @internal */ - public _internalGetTraceContextSigningSecret(): string | undefined { - return ( - this.traceContextSigningSecret ?? - this.loginToken ?? - iso.getEnv("BRAINTRUST_API_KEY") - )?.trim(); - } - - public resetLoginInfo() { - this.loginGeneration++; - this.loginQueueTail = undefined; - this.pendingLogins = new WeakMap(); - this.projectMetadataCache.clear(); - this.appUrl = null; - this.appPublicUrl = null; - this.loginToken = null; - this.orgId = null; - this.orgName = null; - this.apiUrl = null; - this.proxyUrl = null; - this.loggedIn = false; - this.gitMetadataSettings = undefined; - - this._appConn = null; - this._apiConn = null; - this._proxyConn = null; - this.loaderLoginCache = new WeakMap(); - } - - /** @internal */ - public async _internalResolveLoaderLoginOptions({ - apiKey, - appUrl, - orgName, - fetch, - forceLogin, - }: Pick & { - forceLogin?: boolean; - }): Promise { - const resolvedAppUrl = - appUrl ?? - (this.loggedIn ? (this.appUrl ?? undefined) : undefined) ?? - this.loginParams.appUrl ?? - iso.getEnv("BRAINTRUST_APP_URL") ?? - "https://www.braintrust.dev"; - const resolvedApiKey = - apiKey ?? - (this.loggedIn ? (this.loginToken ?? undefined) : undefined) ?? - this.loginParams.apiKey ?? - (await iso.getBraintrustApiKey()); - if (!resolvedApiKey) { - throw new Error( - "Please specify an api key (e.g. by setting BRAINTRUST_API_KEY).", - ); - } - const normalizedApiKey = HTTPConnection.sanitize_token(resolvedApiKey); - const usesActiveCredential = - this.loggedIn && normalizedApiKey === this.loginToken; - const requestedOrgName = - orgName ?? - (usesActiveCredential ? this.activeLoginOrgNameSelector : undefined) ?? - this.loginParams.orgName ?? - iso.getEnv("BRAINTRUST_ORG_NAME"); - const resolvedOrgName = - orgName ?? - (usesActiveCredential ? (this.orgName ?? undefined) : undefined) ?? - requestedOrgName; - const resolvedFetch = - fetch ?? - (this.loggedIn ? this.fetch : undefined) ?? - this.loginParams.fetch ?? - globalThis.fetch; - const credentialCacheNamespace = JSON.stringify([ - "loader-credential", - resolvedAppUrl, - requestedOrgName, - normalizedApiKey, - ]); - - return { - apiKey: normalizedApiKey, - appUrl: resolvedAppUrl, - orgName: resolvedOrgName, - fetch: resolvedFetch, - forceLogin, - credentialCacheNamespace, - existingState: - !forceLogin && - usesActiveCredential && - resolvedAppUrl === this.appUrl && - resolvedOrgName === this.orgName && - resolvedFetch === this.fetch - ? this - : undefined, - }; - } - - /** @internal */ - public _internalGetLoaderCacheViews( - loginOptions: ResolvedLoaderLoginOptions, - requestState?: LoaderRequestState, - ): LoaderCacheViews { - const expectedResolvedOrgIdentity = - requestState?.orgId && requestState.appUrl - ? JSON.stringify([ - "loader-org", - requestState.appUrl, - requestState.orgId, - ]) - : undefined; - - return { - promptCache: this.promptCache.withNamespace( - loginOptions.credentialCacheNamespace, - expectedResolvedOrgIdentity, - ), - parametersCache: this.parametersCache.withNamespace( - loginOptions.credentialCacheNamespace, - expectedResolvedOrgIdentity, - ), - }; - } - - /** @internal */ - public async _internalGetLoaderState({ - apiKey, - appUrl, - orgName, - fetch, - forceLogin, - existingState, - }: ResolvedLoaderLoginOptions) { - if (existingState) { - return existingState; - } - - let cache = this.loaderLoginCache.get(fetch); - if (!cache) { - cache = new LRUCache({ max: LOADER_LOGIN_CACHE_MAX }); - this.loaderLoginCache.set(fetch, cache); - } - - const cacheKey = JSON.stringify([appUrl, orgName, apiKey]); - if (!forceLogin) { - const cachedState = cache.get(cacheKey); - if (cachedState) { - return cachedState; - } - } - - const statePromise = loginToLoaderRequestState({ - orgName, - apiKey, - appUrl, - fetch, - }); - cache.set(cacheKey, statePromise); - - try { - return await statePromise; - } catch (error) { - if (cache.get(cacheKey) === statePromise) { - cache.delete(cacheKey); - } - throw error; - } - } - - public resetIdGenState() { - // Reset the ID generator so it gets recreated with current environment variables - this._idGenerator = null; - } - - public [RESET_CONTEXT_MANAGER_STATE]() { - this._contextManager = null; - } - - public get idGenerator(): IDGenerator { - if (this._idGenerator === null) { - this._idGenerator = getIdGenerator(); - } - return this._idGenerator; - } - - public get contextManager(): ContextManager { - if (this._contextManager === null) { - this._contextManager = getContextManager(); - } - return this._contextManager; - } - - /** - * Register an OTEL flush callback. This is called by @braintrust/otel - * when it initializes a BraintrustSpanProcessor/Exporter. - */ - public registerOtelFlush(callback: () => Promise): void { - this._otelFlushCallback = callback; - } - - /** - * Flush OTEL spans if a callback is registered. - * Called during ensureSpansFlushed to ensure OTEL spans are visible in BTQL. - */ - public async flushOtel(): Promise { - if (this._otelFlushCallback) { - await this._otelFlushCallback(); - } - } - - public copyLoginInfo(other: BraintrustState) { - this.projectMetadataCache.clear(); - this.appUrl = other.appUrl; - this.appPublicUrl = other.appPublicUrl; - this.loginToken = other.loginToken; - this.orgId = other.orgId; - this.orgName = other.orgName; - this.apiUrl = other.apiUrl; - this.proxyUrl = other.proxyUrl; - this.loggedIn = other.loggedIn; - this.gitMetadataSettings = other.gitMetadataSettings; - this.debugLogLevel = other.debugLogLevel; - this.debugLogLevelConfigured = other.debugLogLevelConfigured; - this.traceContextSigningSecret = other.traceContextSigningSecret; - this.fetch = other.fetch; - this.activeLoginOrgNameSelector = other.activeLoginOrgNameSelector; - setGlobalDebugLogLevel( - this.debugLogLevelConfigured ? (this.debugLogLevel ?? false) : undefined, - ); - - this._appConn = other._appConn; - this._apiConn = other._apiConn; - this.loginReplaceApiConn(this.apiConn()); - this._proxyConn = other._proxyConn; - } - - public serialize(): SerializedBraintrustState { - if (!this.loggedIn) { - throw new Error( - "Cannot serialize BraintrustState without being logged in", - ); - } - - if ( - !this.appUrl || - !this.appPublicUrl || - !this.apiUrl || - !this.proxyUrl || - !this.orgName || - !this.loginToken || - !this.loggedIn - ) { - throw new Error( - "Cannot serialize BraintrustState without all login attributes", - ); - } - - return { - appUrl: this.appUrl, - appPublicUrl: this.appPublicUrl, - loginToken: this.loginToken, - orgId: this.orgId, - orgName: this.orgName, - apiUrl: this.apiUrl, - proxyUrl: this.proxyUrl, - gitMetadataSettings: this.gitMetadataSettings, - ...(this.debugLogLevel ? { debugLogLevel: this.debugLogLevel } : {}), - ...(this.debugLogLevelConfigured && !this.debugLogLevel - ? { debugLogLevelDisabled: true } - : {}), - }; - } - - static deserialize( - serialized: unknown, - opts?: LoginOptions, - ): BraintrustState { - const serializedParsed = loginSchema.safeParse(serialized); - if (!serializedParsed.success) { - throw new Error( - `Cannot deserialize BraintrustState: ${serializedParsed.error.message}`, - ); - } - const state = new BraintrustState({ ...opts }); - for (const key of Object.keys(loginSchema.shape)) { - (state as any)[key] = (serializedParsed.data as any)[key]; - } - - if (!state.loginToken) { - throw new Error( - "Cannot deserialize BraintrustState without a login token", - ); - } - - state.apiConn().set_token(state.loginToken); - state.apiConn().make_long_lived(); - state.appConn().set_token(state.loginToken); - if (state.proxyUrl) { - state.proxyConn().make_long_lived(); - state.proxyConn().set_token(state.loginToken); - } - - state.loggedIn = true; - state.debugLogLevelConfigured = - "debugLogLevel" in serializedParsed.data || - !!serializedParsed.data.debugLogLevelDisabled; - setGlobalDebugLogLevel( - state.debugLogLevelConfigured - ? (state.debugLogLevel ?? false) - : undefined, - ); - state.loginReplaceApiConn(state.apiConn()); - - return state; - } - - public setFetch(fetch: typeof globalThis.fetch) { - if (fetch !== this.fetch) { - this.projectMetadataCache.clear(); - } - this.loginParams.fetch = fetch; - this.fetch = fetch; - this._apiConn?.setFetch(fetch); - this._appConn?.setFetch(fetch); - } - - public setMaskingFunction( - maskingFunction: ((value: unknown) => unknown) | null, - ): void { - this.bgLogger().setMaskingFunction(maskingFunction); - } - - public setDebugLogLevel(option: DebugLogLevelOption): void { - if (option === undefined) { - return; - } - this.debugLogLevelConfigured = true; - this.debugLogLevel = normalizeDebugLogLevelOption(option); - setGlobalDebugLogLevel(this.debugLogLevel ?? false); - } - - public getDebugLogLevel(): DebugLogLevel | undefined { - return this.debugLogLevel; - } - - public hasDebugLogLevelOverride(): boolean { - return this.debugLogLevelConfigured; - } - - public async login(loginParams: LoginOptions & { forceLogin?: boolean }) { - this._internalSetTraceContextSigningSecret(loginParams.apiKey); - this.setDebugLogLevel(loginParams.debugLogLevel); - if (this.apiUrl && !loginParams.forceLogin) { - return; - } - const generation = this.loginGeneration; - const pendingLogins = this.pendingLogins; - const options = { - ...this.loginParams, - ...Object.fromEntries( - Object.entries(loginParams).filter(([k, v]) => !isEmpty(v)), - ), - }; - const appUrl = - options.appUrl ?? - (iso.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev"); - const orgName = options.orgName ?? iso.getEnv("BRAINTRUST_ORG_NAME"); - const fetch = options.fetch ?? globalThis.fetch; - const apiKey = options.apiKey ?? (await iso.getBraintrustApiKey()); - if (generation !== this.loginGeneration) { - throw new Error("Login cancelled by a reset."); - } - // Another attempt may have finished while the credential was being read. - if (this.apiUrl && !loginParams.forceLogin) { - if ( - this.appUrl !== appUrl || - this.loginToken !== (apiKey && HTTPConnection.sanitize_token(apiKey)) || - this.activeLoginOrgNameSelector !== orgName || - this.fetch !== fetch - ) { - throw new Error( - "Another login completed with different options during credential discovery. To force re-login, pass `forceLogin: true`.", - ); - } - return; - } - - let pending = pendingLogins.get(fetch); - if (!pending) { - pending = new Map(); - pendingLogins.set(fetch, pending); - } - const key = JSON.stringify([ - appUrl, - orgName, - apiKey, - options.debugLogLevel, - ]); - const existing = pending.get(key); - if (!loginParams.forceLogin && existing) { - return existing; - } - - const previousLogin = this.loginQueueTail; - const loginPromise = (async () => { - // Each forced login still makes a fresh request, but must not invalidate - // the metadata promises of loggers already waiting for another login. - if (previousLogin) { - await previousLogin.catch(() => {}); - } - if (generation !== this.loginGeneration) { - throw new Error("Login cancelled by a reset."); - } - const newState = await loginToState({ - ...options, - appUrl, - orgName, - apiKey, - fetch, - }); - if (generation !== this.loginGeneration) { - throw new Error("Login cancelled by a reset."); - } - this.copyLoginInfo(newState); - })(); - this.loginQueueTail = loginPromise; - pending.set(key, loginPromise); - try { - await loginPromise; - } finally { - if (this.loginQueueTail === loginPromise) { - this.loginQueueTail = undefined; - } - if (pending.get(key) === loginPromise) { - pending.delete(key); - } - } - } - - public appConn(): HTTPConnection { - if (!this._appConn) { - if (!this.appUrl) { - throw new Error("Must initialize appUrl before requesting appConn"); - } - this._appConn = new HTTPConnection(this.appUrl, this.fetch); - } - return this._appConn!; - } - - public apiConn(): HTTPConnection { - if (!this._apiConn) { - if (!this.apiUrl) { - throw new Error("Must initialize apiUrl before requesting apiConn"); - } - this._apiConn = new HTTPConnection(this.apiUrl, this.fetch); - } - return this._apiConn!; - } - - public proxyConn(): HTTPConnection { - if (!this.proxyUrl) { - return this.apiConn(); - } - if (!this._proxyConn) { - if (!this.proxyUrl) { - throw new Error("Must initialize proxyUrl before requesting proxyConn"); - } - this._proxyConn = new HTTPConnection( - normalizeProxyConnUrl(this.proxyUrl), - this.fetch, - ); - } - return this._proxyConn!; - } - - public bgLogger(): BackgroundLogger { - if (this._overrideBgLogger) { - return this._overrideBgLogger; - } - return this._bgLogger.get(); - } - - public httpLogger(): HTTPBackgroundLogger { - // this is called for configuration in some end-to-end tests so - // expose the http bg logger here. - return this._bgLogger.get(); - } - - public setOverrideBgLogger(logger: BackgroundLogger | null) { - this._overrideBgLogger = logger; - } - - // Should only be called by the login function. - public loginReplaceApiConn(apiConn: HTTPConnection) { - this._bgLogger.get().internalReplaceApiConn(apiConn); - } - - public disable() { - this._bgLogger.get().disable(); - } - - public enforceQueueSizeLimit(enforce: boolean) { - this._bgLogger.get().enforceQueueSizeLimit(enforce); - } - - // Custom serialization to avoid logging sensitive data - toJSON(): Record { - return { - id: this.id, - orgId: this.orgId, - orgName: this.orgName, - appUrl: this.appUrl, - appPublicUrl: this.appPublicUrl, - apiUrl: this.apiUrl, - proxyUrl: this.proxyUrl, - loggedIn: this.loggedIn, - // Explicitly exclude loginToken, _apiConn, _appConn, _proxyConn and other sensitive fields - }; - } - - // Custom inspect for Node.js console.log - [Symbol.for("nodejs.util.inspect.custom")](): string { - return `BraintrustState { - id: '${this.id}', - orgId: ${this.orgId ? `'${this.orgId}'` : "null"}, - orgName: ${this.orgName ? `'${this.orgName}'` : "null"}, - appUrl: ${this.appUrl ? `'${this.appUrl}'` : "null"}, - apiUrl: ${this.apiUrl ? `'${this.apiUrl}'` : "null"}, - proxyUrl: ${this.proxyUrl ? `'${this.proxyUrl}'` : "null"}, - loggedIn: ${this.loggedIn}, - loginToken: '[REDACTED]' -}`; - } - - // Custom toString - toString(): string { - return `BraintrustState(id=${this.id}, org=${this.orgName || "none"}, loggedIn=${this.loggedIn})`; - } -} - -let _globalState: BraintrustState; - -// Return a TestBackgroundLogger that will intercept logs before they are sent to the server. -// Used for testing only. -function useTestBackgroundLogger(): TestBackgroundLogger { - const state = _internalGetGlobalState(); - if (!state) { - throw new Error("global state not set yet"); - } - - const logger = new TestBackgroundLogger(); - state.setOverrideBgLogger(logger); - return logger; -} - -function clearTestBackgroundLogger() { - _internalGetGlobalState()?.setOverrideBgLogger(null); -} - -// Initialize a test Experiment without making network calls by injecting fake metadata. -// Useful for unit tests that need an Experiment instance. -function initTestExperiment( - experimentName: string, - projectName?: string, - experimentFullInfo: Record = {}, -): Experiment { - setInitialTestState(); - const state = _internalGetGlobalState(); - const project = projectName ?? experimentName; - - const lazyMetadata: LazyValue = new LazyValue( - async () => ({ - project: { id: project, name: project, fullInfo: {} }, - experiment: { - id: experimentName, - name: experimentName, - fullInfo: experimentFullInfo, - }, - }), - ); - - return new Experiment(state, lazyMetadata); -} - -/** - * Initialize the global Braintrust state lazily, it only creates the BraintrustState instance on the first invocation. - * - * The state is stored in a global symbol to ensure cross-bundle compatibility. - * - * This is invoked by platform-specific initialization functions (configureNode/configureBrowser) - * - * @internal - */ -export function _internalSetInitialState() { - if (_globalState) { - return; - } - const sym = Symbol.for("braintrust-state"); - let existing = (globalThis as any)[sym]; - if (!existing) { - const state = new BraintrustState({}); - (globalThis as any)[sym] = state; - existing = state; - } - _globalState = existing; -} -/** - * @internal - */ -export const _internalGetGlobalState = () => _globalState; -setDebugLogStateResolver(() => _internalGetGlobalState()); - -/** - * Register a callback to flush OTEL spans. This is called by @braintrust/otel - * when it initializes a BraintrustSpanProcessor/Exporter. - * - * When ensureSpansFlushed is called (e.g., before a BTQL query in scorers), - * this callback will be invoked to ensure OTEL spans are flushed to the server. - * - * Also disables the span cache, since OTEL spans aren't in the local cache - * and we need BTQL to see the complete span tree (both native + OTEL spans). - */ -export function registerOtelFlush(callback: () => Promise): void { - _globalState?.registerOtelFlush(callback); - // Disable span cache since OTEL spans aren't in the local cache - _globalState?.spanCache?.disable(); -} - -export class FailedHTTPResponse extends Error { - public status: number; - public text: string; - public data: string; - public readonly cause?: unknown; - - constructor(status: number, text: string, data: string, cause?: unknown) { - super(`${status}: ${text} (${data})`); - this.status = status; - this.text = text; - this.data = data; - this.cause = cause; - } -} - -class HTTPTransportError extends Error { - public readonly cause: unknown; - - constructor(cause: unknown) { - super(cause instanceof Error ? cause.message : String(cause)); - this.name = "HTTPTransportError"; - this.cause = cause; - } -} - -const httpTransportErrorCauses = new WeakSet(); - -function recordHTTPTransportError(error: unknown): void { - if ( - (typeof error === "object" && error !== null) || - typeof error === "function" - ) { - httpTransportErrorCauses.add(error); - } -} - -function rethrowHTTPTransportError( - error: unknown, - classifyTransportErrors: boolean, -): never { - if (classifyTransportErrors) { - throw new HTTPTransportError(error); - } - recordHTTPTransportError(error); - throw error; -} - -function isLoaderCacheFallbackError(error: unknown): boolean { - if (error instanceof FailedHTTPResponse) { - return error.status === 408 || error.status === 429 || error.status >= 500; - } - - if (error instanceof HTTPTransportError) { - return true; - } - - return ( - ((typeof error === "object" && error !== null) || - typeof error === "function") && - httpTransportErrorCauses.has(error) - ); -} - -async function readJSONResponse( - response: Response, - classifyTransportErrors = false, -) { - let data: string; - try { - data = await response.text(); - } catch (error) { - rethrowHTTPTransportError(error, classifyTransportErrors); - } - return JSON.parse(data); -} - -async function checkResponse(resp: Response) { - if (resp.ok) { - return resp; - } - - let data: string; - try { - data = await resp.text(); - } catch (error) { - throw new FailedHTTPResponse( - resp.status, - resp.statusText, - "Unable to read response body", - error, - ); - } - throw new FailedHTTPResponse(resp.status, resp.statusText, data); -} - -class HTTPConnection { - base_url: string; - token: string | null; - headers: Record; - fetch: typeof globalThis.fetch; - - constructor( - base_url: string, - fetch: typeof globalThis.fetch, - private readonly classifyTransportErrors = false, - ) { - this.base_url = base_url; - this.token = null; - this.headers = {}; - - this._reset(); - this.fetch = fetch; - } - - public setFetch(fetch: typeof globalThis.fetch) { - this.fetch = fetch; - } - - async ping() { - try { - const resp = await this.get("ping"); - return resp.status === 200; - } catch { - return false; - } - } - - make_long_lived() { - // Following a suggestion in https://stackoverflow.com/questions/23013220/max-retries-exceeded-with-url-in-requests - this._reset(); - } - - static sanitize_token(token: string) { - return token.trim(); - } - - set_token(token: string) { - token = HTTPConnection.sanitize_token(token); - this.token = token; - this._reset(); - } - - // As far as I can tell, you cannot set the retry/backoff factor here - _reset() { - this.headers = {}; - if (this.token) { - this.headers["Authorization"] = `Bearer ${this.token}`; - } - } - - async get( - path: string, - params: - | Record - | undefined = undefined, - config?: RequestInit, - ) { - const { headers, ...rest } = config || {}; - const url = new URL(_urljoin(this.base_url, path)); - url.search = new URLSearchParams( - params - ? Object.entries(params) - .filter(([_, v]) => v !== undefined) - .flatMap(([k, v]) => - v !== undefined - ? typeof v === "string" - ? [[k, v]] - : v.map((x) => [k, x]) - : [], - ) - : [], - ).toString(); - - // On platforms like Cloudflare, we lose "this" when we make an async call, - // so we need to bind it again. - const this_fetch = this.fetch; - const this_headers = this.headers; - let response: Response; - try { - // Using toString() here makes it work with isomorphic fetch - response = await this_fetch(url.toString(), { - headers: { - Accept: "application/json", - ...this_headers, - ...headers, - }, - keepalive: true, - ...rest, - }); - } catch (error) { - if (config?.signal?.aborted) { - throw getAbortReason(config.signal); - } - rethrowHTTPTransportError(error, this.classifyTransportErrors); - } - return await checkResponse(response); - } - - async post( - path: string, - params?: Record | string, - config?: RequestInit, - retries: number = 0, - ) { - const { headers, ...rest } = config || {}; - // On platforms like Cloudflare, we lose "this" when we make an async call, - // so we need to bind it again. - const this_fetch = this.fetch; - const this_base_url = this.base_url; - const this_headers = this.headers; - - const tries = retries + 1; - for (let i = 0; i < tries; i++) { - try { - let response: Response; - try { - response = await this_fetch(_urljoin(this_base_url, path), { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - ...this_headers, - ...headers, - }, - body: - typeof params === "string" - ? params - : params - ? JSON.stringify(params) - : undefined, - keepalive: true, - ...rest, - }); - } catch (error) { - if (config?.signal?.aborted) { - throw getAbortReason(config.signal); - } - rethrowHTTPTransportError(error, this.classifyTransportErrors); - } - return await checkResponse(response); - } catch (error) { - if (config?.signal?.aborted) { - throw getAbortReason(config.signal); - } - if (i === tries - 1 || !isRetryableHTTPError(error)) { - throw error; - } - - debugLogger.debug( - `Retrying API request ${path} after ${formatHTTPError(error)}`, - ); - const sleepTimeMs = - HTTP_RETRY_BASE_SLEEP_TIME_S * 1000 * 2 ** i + - Math.random() * HTTP_RETRY_JITTER_MS; - debugLogger.info( - `Sleeping for ${sleepTimeMs}ms before retrying API request`, - ); - await waitForRetry(sleepTimeMs, config?.signal); - } - } - - throw new Error("Unexpected retry state"); - } - - async get_json( - object_type: string, - args: Record | undefined = undefined, - retries: number = 0, - signal?: AbortSignal, - ) { - const tries = retries + 1; - for (let i = 0; i < tries; i++) { - try { - const resp = await this.get(`${object_type}`, args, { signal }); - return await readJSONResponse(resp, this.classifyTransportErrors); - } catch (e) { - if (signal?.aborted) { - throw getAbortReason(signal); - } - if (i < tries - 1) { - debugLogger.debug( - `Retrying API request ${object_type} ${JSON.stringify(args)} ${ - (e as any).status - } ${(e as any).text}`, - ); - const sleepTimeS = HTTP_RETRY_BASE_SLEEP_TIME_S * 2 ** i; - debugLogger.info( - `Sleeping for ${sleepTimeS}s before retrying API request`, - ); - await waitForRetry(sleepTimeS * 1000, signal); - continue; - } - throw e; - } - } - } - - async post_json( - object_type: string, - args: Record | string | undefined = undefined, - signal?: AbortSignal, - ) { - const resp = await this.post(`${object_type}`, args, { - headers: { "Content-Type": "application/json" }, - signal, - }); - return await readJSONResponse(resp, this.classifyTransportErrors); - } - - // Custom inspect for Node.js console.log - [Symbol.for("nodejs.util.inspect.custom")](): string { - return `HTTPConnection { - base_url: '${this.base_url}', - token: '[REDACTED]' -}`; - } - - // Custom toString - toString(): string { - return `HTTPConnection(${this.base_url})`; - } -} - -export interface ObjectMetadata { - id: string; - name: string; - fullInfo: Record; -} - -interface ProjectExperimentMetadata { - project: ObjectMetadata; - experiment: ObjectMetadata; -} - -interface ProjectDatasetMetadata { - project: ObjectMetadata; - dataset: ObjectMetadata; -} - -interface OrgProjectMetadata { - org_id: string; - project: ObjectMetadata; -} - -interface LinkArgs { - org_name?: string; - app_url?: string; -} - -type ProjectMetadataArgs = { - project_group_name?: string; - project_name?: string; - project_id?: string; -}; - -export interface AttachmentParams { - data: string | Blob | ArrayBuffer; - filename: string; - contentType: string; - state?: BraintrustState; -} - -export interface ExternalAttachmentParams { - url: string; - filename: string; - contentType: string; - state?: BraintrustState; -} - -export abstract class BaseAttachment { - readonly reference!: AttachmentReference; - abstract upload(): Promise; - abstract data(): Promise; - abstract debugInfo(): Record; -} - -type DatasetPipelineDeferredJSONAttachmentHook = ( - data: unknown, - options?: { filename?: string; pretty?: boolean }, -) => object; - -declare global { - // Set by the bt dataset pipeline runner so JSONAttachment can be represented - // as a destination-uploaded marker during transform. - // eslint-disable-next-line no-var - var __BT_DATASET_PIPELINE_DEFER_JSON_ATTACHMENT__: - | DatasetPipelineDeferredJSONAttachmentHook - | undefined; -} - -/** - * Represents an attachment to be uploaded and the associated metadata. - * `Attachment` objects can be inserted anywhere in an event, allowing you to - * log arbitrary file data. The SDK will asynchronously upload the file to - * object storage and replace the `Attachment` object with an - * `AttachmentReference`. - */ -export class Attachment extends BaseAttachment { - /** - * The object that replaces this `Attachment` at upload time. - */ - readonly reference: BraintrustAttachmentReference; - - private readonly uploader: LazyValue; - private readonly _data: LazyValue; - private readonly state?: BraintrustState; - // For debug logging only. - private readonly dataDebugString: string; - - /** - * Construct an attachment. - * - * @param param A parameter object with: - * - * `data`: A string representing the path of the file on disk, or a - * `Blob`/`ArrayBuffer` with the file's contents. The caller is responsible - * for ensuring the file/blob/buffer is not modified until upload is complete. - * - * `filename`: The desired name of the file in Braintrust after uploading. - * This parameter is for visualization purposes only and has no effect on - * attachment storage. - * - * `contentType`: The MIME type of the file. - * - * `state`: (Optional) For internal use. - */ - constructor({ data, filename, contentType, state }: AttachmentParams) { - super(); - this.reference = { - type: BRAINTRUST_ATTACHMENT, - filename, - content_type: contentType, - key: newId(), - }; - this.state = state; - this.dataDebugString = typeof data === "string" ? data : ""; - - this._data = this.initData(data); - this.uploader = this.initUploader(); - } - - /** - * On first access, (1) reads the attachment from disk if needed, (2) - * authenticates with the data plane to request a signed URL, (3) uploads to - * object store, and (4) updates the attachment. - * - * @returns The attachment status. - */ - async upload() { - return await this.uploader.get(); - } - - /** - * The attachment contents. This is a lazy value that will read the attachment contents from disk or memory on first access. - */ - async data() { - return this._data.get(); - } - - /** - * A human-readable description for logging and debugging. - * - * @returns The debug object. The return type is not stable and may change in - * a future release. - */ - debugInfo(): Record { - return { - inputData: this.dataDebugString, - reference: this.reference, - state: this.state, - }; - } - - private initUploader(): LazyValue { - const doUpload = async (conn: HTTPConnection, orgId: string) => { - const requestParams = { - key: this.reference.key, - filename: this.reference.filename, - content_type: this.reference.content_type, - org_id: orgId, - }; - const [metadataPromiseResult, dataPromiseResult] = - await Promise.allSettled([ - conn.post("/attachment", requestParams), - this._data.get(), - ]); - if (metadataPromiseResult.status === "rejected") { - const errorStr = JSON.stringify(metadataPromiseResult.reason); - throw new Error( - `Failed to request signed URL from API server: ${errorStr}`, - ); - } - if (dataPromiseResult.status === "rejected") { - const errorStr = JSON.stringify(dataPromiseResult.reason); - throw new Error(`Failed to read file: ${errorStr}`); - } - const metadataResponse = metadataPromiseResult.value; - const data = dataPromiseResult.value; - - let signedUrl: string | undefined; - let headers: Record; - try { - ({ signedUrl, headers } = z - .object({ - signedUrl: z.string().url(), - headers: z.record(z.string()), - }) - .parse(await metadataResponse.json())); - } catch (error) { - if (error instanceof ZodError) { - const errorStr = JSON.stringify(error.flatten()); - throw new Error(`Invalid response from API server: ${errorStr}`); - } - throw error; - } - - addAzureBlobHeaders(headers, signedUrl); - - // TODO multipart upload. - let objectStoreResponse: Response | undefined; - try { - objectStoreResponse = await checkResponse( - await fetch(signedUrl, { - method: "PUT" satisfies RequestInit["method"], - headers, - body: data, - }), - ); - await objectStoreResponse.body?.cancel(); - } catch (error) { - if (error instanceof FailedHTTPResponse) { - throw new Error( - `Failed to upload attachment to object store: ${error.status} ${error.text} ${error.data}`, - ); - } - throw error; - } - - return { signedUrl, metadataResponse, objectStoreResponse }; - }; - - // Catches error messages and updates the attachment status. - const errorWrapper = async () => { - const status: AttachmentStatus = { upload_status: "done" }; - - const state = this.state ?? _globalState; - await state.login({}); - - const conn = state.apiConn(); - const orgId = state.orgId ?? ""; - - try { - await doUpload(conn, orgId); - } catch (error) { - status.upload_status = "error"; - status.error_message = - error instanceof Error ? error.message : JSON.stringify(error); - } - - const requestParams = { - key: this.reference.key, - org_id: orgId, - status, - }; - const statusResponse = await conn.post( - "/attachment/status", - requestParams, - ); - await statusResponse.body?.cancel(); - if (!statusResponse.ok) { - const errorStr = JSON.stringify(statusResponse); - throw new Error(`Couldn't log attachment status: ${errorStr}`); - } - - return status; - }; - - return new LazyValue(errorWrapper); - } - - private initData(data: string | Blob | ArrayBuffer): LazyValue { - if (typeof data === "string") { - this.ensureFileReadable(data); - - const readFile = iso.readFile; - if (!readFile) { - throw new Error( - `This platform does not support reading the filesystem. Construct the Attachment -with a Blob/ArrayBuffer, or run the program on Node.js.`, - ); - } - // This could stream the file in the future. - return new LazyValue(async () => new Blob([await readFile(data)])); - } else { - return new LazyValue(async () => new Blob([data])); - } - } - - private ensureFileReadable(data: string) { - const statSync = iso.statSync; - if (!statSync) { - throw new Error( - `This platform does not support reading the filesystem. Construct the Attachment -with a Blob/ArrayBuffer, or run the program on Node.js.`, - ); - } - - try { - statSync(data); - } catch (e) { - debugLogger.warn(`Failed to read file: ${e}`); - } - } -} - -/** - * Represents an attachment that resides in an external object store and the associated metadata. - * - * `ExternalAttachment` objects can be inserted anywhere in an event, similar to - * `Attachment` objects, but they reference files that already exist in an external - * object store rather than requiring upload. The SDK will replace the `ExternalAttachment` - * object with an `AttachmentReference` during logging. - */ -export class ExternalAttachment extends BaseAttachment { - /** - * The object that replaces this `ExternalAttachment` at upload time. - */ - readonly reference: ExternalAttachmentReference; - - private readonly _data: LazyValue; - private readonly state?: BraintrustState; - - /** - * Construct an external attachment. - * - * @param param A parameter object with: - * - * `url`: The fully qualified URL of the file in the external object store. - * - * `filename`: The desired name of the file in Braintrust after uploading. - * This parameter is for visualization purposes only and has no effect on - * attachment storage. - * - * `contentType`: The MIME type of the file. - * - * `state`: (Optional) For internal use. - */ - constructor({ url, filename, contentType, state }: ExternalAttachmentParams) { - super(); - this.reference = { - type: EXTERNAL_ATTACHMENT, - filename, - content_type: contentType, - url, - }; - - this._data = this.initData(); - } - - /** - * For ExternalAttachment, this is a no-op since the data already resides - * in the external object store. It marks the attachment as already uploaded. - * - * @returns The attachment status, which will always indicate success. - */ - async upload() { - return { upload_status: "done" as const }; - } - - /** - * The attachment contents. This is a lazy value that will read the attachment contents from the external object store on first access. - */ - async data() { - return this._data.get(); - } - - /** - * A human-readable description for logging and debugging. - * - * @returns The debug object. The return type is not stable and may change in - * a future release. - */ - debugInfo(): Record { - return { - url: this.reference.url, - reference: this.reference, - state: this.state, - }; - } - - private initData(): LazyValue { - return new LazyValue(async () => { - const readonly = new ReadonlyAttachment(this.reference, this.state); - return await readonly.data(); - }); - } -} - -const attachmentMetadataSchema = z.object({ - downloadUrl: z.string(), - status: attachmentStatusSchema, -}); - -type AttachmentMetadata = { - downloadUrl: string; - status: AttachmentStatus; -}; - -/** - * A readonly alternative to `Attachment`, which can be used for fetching - * already-uploaded Attachments. - */ -export class ReadonlyAttachment { - /** - * Attachment metadata. - */ - readonly reference: AttachmentReference; - - private readonly _data: LazyValue; - private readonly state?: BraintrustState; - - /** - * Construct a ReadonlyAttachment. - * - * @param reference The `AttachmentReference` that should be read by the - * `ReadonlyAttachment` object. - * @param state (Optional) For internal use. - * @returns The new `ReadonlyAttachment` object. - */ - constructor(reference: AttachmentReference, state?: BraintrustState) { - this.reference = reference; - this.state = state; - this._data = this.initDownloader(); - } - - /** - * The attachment contents. This is a lazy value that will read the attachment - * contents from the object store on first access. - */ - async data() { - return this._data.get(); - } - - /** - * Returns the attachment contents as a base64-encoded URL that is suitable - * for use in a prompt. - * - * @returns The attachment contents as a base64-encoded URL. - */ - async asBase64Url(): Promise { - const buf = await (await this.data()).arrayBuffer(); - const base64 = Buffer.from(buf).toString("base64"); - return `data:${this.reference.content_type};base64,${base64}`; - } - - /** - * Fetch the attachment metadata, which includes a downloadUrl and a status. - * This will re-fetch the status each time in case it changes over time. - */ - async metadata(): Promise { - const state = this.state ?? _globalState; - await state.login({}); - - const params: Record = { - filename: this.reference.filename, - content_type: this.reference.content_type, - org_id: state.orgId || "", - }; - if (this.reference.type === "braintrust_attachment") { - params.key = this.reference.key; - } else if (this.reference.type === "external_attachment") { - params.url = this.reference.url; - } - const resp = await state.apiConn().get("/attachment", params); - if (!resp.ok) { - const errorStr = JSON.stringify(resp); - throw new Error(`Invalid response from API server: ${errorStr}`); - } - - return attachmentMetadataSchema.parse(await resp.json()); - } - - /** - * Fetch the attachment upload status. This will re-fetch the status each time - * in case it changes over time. - */ - async status(): Promise { - return (await this.metadata()).status; - } - - private initDownloader(): LazyValue { - const download = async () => { - const { downloadUrl, status } = await this.metadata(); - - if (status.upload_status !== "done") { - throw new Error( - `Expected attachment status "done", got "${status.upload_status}"`, - ); - } - - const objResponse = await fetch(downloadUrl); - if (objResponse.status !== 200) { - const error = await objResponse.text(); - throw new Error(`Couldn't download attachment: ${error}`); - } - - return await objResponse.blob(); - }; - - return new LazyValue(download); - } -} - -/** - * Represents a JSON object that should be stored as an attachment. - * - * `JSONAttachment` is a convenience function that creates an `Attachment` - * from JSON data. It's particularly useful for large JSON objects that - * would otherwise bloat the trace size. - * - * The JSON data is automatically serialized and stored as an attachment - * with content type "application/json". - */ -export class JSONAttachment extends Attachment { - /** - * Construct a JSONAttachment from a JSON-serializable object. - * - * @param data The JSON object to attach. Must be JSON-serializable. - * @param options Additional options: - * - `filename`: The filename for the attachment (defaults to "data.json") - * - `pretty`: Whether to pretty-print the JSON (defaults to false) - * - `state`: (Optional) For internal use. - * - * @example - * ```typescript - * const largeTranscript = [ - * { role: "user", content: "..." }, - * { role: "assistant", content: "..." }, - * // ... many more messages - * ]; - * - * logger.log({ - * input: { - * type: "chat", - * transcript: new JSONAttachment(largeTranscript, { filename: "transcript.json" }) - * } - * }); - * ``` - */ - constructor( - data: unknown, - options?: { - filename?: string; - pretty?: boolean; - state?: BraintrustState; - }, - ) { - const { filename = "data.json", pretty = false, state } = options ?? {}; - const deferredJsonAttachment = - globalThis.__BT_DATASET_PIPELINE_DEFER_JSON_ATTACHMENT__; - if (deferredJsonAttachment) { - super({ - data: new Blob([]), - filename, - contentType: "application/json", - state, - }); - return deferredJsonAttachment(data, { - filename, - pretty, - }) as unknown as JSONAttachment; - } - - // Serialize the JSON data - const jsonString = pretty - ? JSON.stringify(data, null, 2) - : JSON.stringify(data); - const blob = new Blob([jsonString], { type: "application/json" }); - - super({ - data: blob, - filename, - contentType: "application/json", - state, - }); - } -} - -function logFeedbackImpl( - state: BraintrustState, - parentObjectType: SpanObjectTypeV3, - parentObjectId: LazyValue, - { - id, - expected, - scores, - metadata: inputMetadata, - tags, - comment, - source: inputSource, - }: LogFeedbackFullArgs, -) { - const source = inputSource ?? "external"; - - if (!VALID_SOURCES.includes(source)) { - throw new Error(`source must be one of ${VALID_SOURCES}`); - } - - if ( - isEmpty(scores) && - isEmpty(expected) && - isEmpty(tags) && - isEmpty(comment) - ) { - throw new Error( - "At least one of scores, expected, tags, or comment must be specified", - ); - } - - const validatedEvent = validateAndSanitizeExperimentLogPartialArgs({ - scores, - metadata: inputMetadata, - expected, - tags, - }); - - const { metadata, ...rawUpdateEvent } = deepCopyEvent(validatedEvent); - const updateEvent = Object.fromEntries( - Object.entries(rawUpdateEvent).filter(([_, v]) => !isEmpty(v)), - ); - - const parentIds = async () => - new SpanComponentsV3({ - object_type: parentObjectType, - object_id: await parentObjectId.get(), - }).objectIdFields(); - - if (Object.keys(updateEvent).length > 0) { - const record = new LazyValue(async () => { - return { - id, - ...updateEvent, - ...(await parentIds()), - [AUDIT_SOURCE_FIELD]: source, - [AUDIT_METADATA_FIELD]: metadata, - [IS_MERGE_FIELD]: true, - }; - }); - state.bgLogger().log([record]); - } - - if (!isEmpty(comment)) { - const record = new LazyValue(async () => { - return { - id: uuidv4(), - created: new Date().toISOString(), - origin: { - // NOTE: We do not know (or care?) what the transaction id of the row that - // we're commenting on is here, so we omit it. - id, - }, - comment: { - text: comment, - }, - ...(await parentIds()), - [AUDIT_SOURCE_FIELD]: source, - [AUDIT_METADATA_FIELD]: metadata, - }; - }); - state.bgLogger().log([record]); - } -} - -function updateSpanImpl({ - state, - parentObjectType, - parentObjectId, - id, - root_span_id, - span_id, - event, -}: { - state: BraintrustState; - parentObjectType: SpanObjectTypeV3; - parentObjectId: LazyValue; - id: string; - root_span_id: string | undefined; - span_id: string | undefined; - event: Omit, "id" | "root_span_id" | "span_id">; -}): void { - if (isEmpty(root_span_id) !== isEmpty(span_id)) { - throw new Error("both root_span_id and span_id must be set, or neither"); - } - const hasExplicitSpanIds = - root_span_id !== undefined && span_id !== undefined; - const updateEvent = deepCopyEvent( - validateAndSanitizeExperimentLogPartialArgs({ - ...event, - id, - ...(hasExplicitSpanIds ? { root_span_id, span_id } : {}), - } as Partial), - ); - - const parentIds = async () => - new SpanComponentsV3({ - object_type: parentObjectType, - object_id: await parentObjectId.get(), - }).objectIdFields(); - - const record = new LazyValue( - async () => - customizeSpanExport({ - id, - ...updateEvent, - ...(await parentIds()), - [IS_MERGE_FIELD]: true, - }) as BackgroundLogEvent | null, - ); - state.bgLogger().log([record]); -} - -/** - * Update a span using the output of `span.export()`. It is important that you only resume updating - * to a span once the original span has been fully written and flushed, since otherwise updates to - * the span may conflict with the original span. - * - * @param exported The output of `span.export()`. - * @param event The event data to update the span with. See {@link Experiment.log} for a full list of valid fields. - * @param state (optional) Login state to use. If not provided, the global state will be used. - */ -export function updateSpan({ - exported, - state, - ...event -}: { exported: string } & Omit< - Partial, - "id" | "root_span_id" | "span_id" -> & - OptionalStateArg): void { - const resolvedState = state ?? _globalState; - const components = SpanComponentsV4.fromStr(exported); - - if (!components.data.row_id) { - throw new Error("Exported span must have a row id"); - } - - updateSpanImpl({ - state: resolvedState, - parentObjectType: components.data.object_type, - parentObjectId: new LazyValue( - spanComponentsToObjectIdLambda(resolvedState, components), - ), - id: components.data.row_id, - root_span_id: components.data.root_span_id, - span_id: components.data.span_id, - event, - }); -} - -/** @internal Rehydrate an exported root span so work can continue in another process. */ -export function _internalResumeSpan({ - exported, - state, -}: { - exported: string; - state?: BraintrustState; -}): Span { - const resolvedState = state ?? _globalState; - const components = SpanComponentsV4.fromStr(exported); - const { row_id, root_span_id, span_id } = components.data; - if (!row_id || !root_span_id || !span_id) { - throw new Error("Only exported root spans can be resumed"); - } - return new SpanImpl({ - state: resolvedState, - parentObjectType: components.data.object_type, - parentObjectId: new LazyValue( - spanComponentsToObjectIdLambda(resolvedState, components), - ), - parentComputeObjectMetadataArgs: undefined, - parentSpanIds: { parentSpanIds: [], rootSpanId: root_span_id }, - spanId: span_id, - event: { id: row_id }, - propagatedEvent: (components.data.propagated_event ?? undefined) as - | StartSpanEventArgs - | undefined, - [RESUME_SPAN_WITHOUT_INITIAL_WRITE]: true, - }); -} - -/** - * An opaque W3C trace-context, as returned by - * {@link extractTraceContextFromHeaders}. - * - * Carries the relevant W3C headers (`traceparent`, `baggage`, `tracestate`). - * Callers MUST treat it as opaque and pass it straight to - * `startSpan({ parent })`. - */ -export type PropagationContext = Record; - -interface ParentSpanIds { - spanId: string; - rootSpanId: string; -} - -interface MultiParentSpanIds { - parentSpanIds: string[]; - rootSpanId: string; -} - -function spanComponentsToObjectIdLambda( - state: BraintrustState, - components: SpanComponentsV3, -): () => Promise { - if (components.data.object_id) { - const ret = components.data.object_id; - return async () => ret; - } - if (!components.data.compute_object_metadata_args) { - throw new Error( - "Impossible: must provide either objectId or computeObjectMetadataArgs", - ); - } - switch (components.data.object_type) { - case SpanObjectTypeV3.EXPERIMENT: - throw new Error( - "Impossible: computeObjectMetadataArgs not supported for experiments", - ); - case SpanObjectTypeV3.PLAYGROUND_LOGS: - throw new Error( - "Impossible: computeObjectMetadataArgs not supported for prompt sessions", - ); - case SpanObjectTypeV3.PROJECT_LOGS: - return async () => - ( - await computeLoggerMetadata(state, { - ...components.data.compute_object_metadata_args, - }) - ).project.id; - default: - const x: never = components.data.object_type; - throw new Error(`Unknown object type: ${x}`); - } -} - -// Utility function to resolve the object ID of a SpanComponentsV3 object. This -// function may trigger a login to braintrust if the object ID is encoded -// "lazily". -export async function spanComponentsToObjectId({ - components, - state, -}: { - components: SpanComponentsV3; - state?: BraintrustState; -}): Promise { - return await spanComponentsToObjectIdLambda( - state ?? _globalState, - components, - )(); -} - -export const ERR_PERMALINK = "https://braintrust.dev/error-generating-link"; - -function getErrPermlink(msg: string) { - if (msg == "") { - return ERR_PERMALINK; - } - return `${ERR_PERMALINK}?msg=${encodeURIComponent(msg)}`; -} - -function _getAppUrl(appUrl?: string | null): string { - return ( - appUrl || iso.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev" - ); -} - -function _getOrgName(orgName?: string | null): string | undefined { - return orgName || iso.getEnv("BRAINTRUST_ORG_NAME") || undefined; -} - -/** - * Return the base URL for links (e.g. https://braintrust.dev/app/my-org-name) - * if we have the info, otherwise return null. - * Resolution order: state -> linkArgs -> env var - */ -function _getLinkBaseUrl( - state: BraintrustState, - linkArgs?: LinkArgs, -): string | null { - const appUrl = _getAppUrl(state.appUrl || linkArgs?.app_url); - const orgName = _getOrgName(state.orgName || linkArgs?.org_name); - if (!orgName) { - return null; - } - return `${appUrl}/app/${orgName}`; -} - -/** - * Format a permalink to the Braintrust application for viewing the span - * represented by the provided `slug`. - * - * Links can be generated at any time, but they will only become viewable after - * the span and its root have been flushed to the server and ingested. - * - * If you have a `Span` object, use {@link Span.link} instead. - * - * @param slug The identifier generated from {@link Span.export}. - * @param opts Optional arguments. - * @param opts.state The login state to use. If not provided, the global state will be used. - * @param opts.orgName The org name to use. If not provided, the org name will be inferred from the state. - * @param opts.appUrl The app URL to use. If not provided, the app URL will be inferred from the state. - * @returns A permalink to the exported span. - */ -export async function permalink( - slug: string, - opts?: { - state?: BraintrustState; - orgName?: string; - appUrl?: string; - }, -): Promise { - // Noop spans have an empty slug, so return a dummy permalink. - if (slug === "") { - return NOOP_SPAN_PERMALINK; - } - - const state = opts?.state ?? _globalState; - const getOrgName = async () => { - if (opts?.orgName) { - return opts.orgName; - } - await state.login({}); - if (!state.orgName) { - throw new Error("provide-org-or-login"); // this is caught below - } - return state.orgName; - }; - const getAppUrl = async () => { - if (opts?.appUrl) { - return opts.appUrl; - } - await state.login({}); - if (!state.appUrl) { - throw new Error("provide-app-url-or-login"); // this is caught below - } - return state.appUrl; - }; - - try { - const components = SpanComponentsV4.fromStr(slug); - const object_type = spanObjectTypeV3ToString(components.data.object_type); - const [orgName, appUrl, object_id] = await Promise.all([ - getOrgName(), - getAppUrl(), - spanComponentsToObjectId({ components, state }), - ]); - const id = components.data.row_id; - if (!id) { - throw new Error("Span slug does not refer to an individual row"); - } - const urlParams = new URLSearchParams({ object_type, object_id, id }); - return `${appUrl}/app/${orgName}/object?${urlParams}`; - } catch (e) { - if (e instanceof FailedHTTPResponse) { - return getErrPermlink(`http-error-${e.status}`); - } - return getErrPermlink(e instanceof Error ? e.message : String(e)); - } -} - -// IMPORTANT NOTE: This function may pass arguments which override those in the -// main argument set, so if using this in a spread, like `SpanImpl({ ...args, -// ...startSpanParentArgs(...)})`, make sure to put startSpanParentArgs after -// the original argument set. -function startSpanParentArgs(args: { - state: BraintrustState; - // `parent` may be an exported slug string or an opaque W3C trace-context - // (from `extractTraceContextFromHeaders`). - parent: string | PropagationContext | undefined; - parentObjectType: SpanObjectTypeV3; - parentObjectId: LazyValue; - parentComputeObjectMetadataArgs: Record | undefined; - parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined; - propagatedEvent: StartSpanEventArgs | undefined; - propagatedState?: PropagatedState | undefined; -}): { - parentObjectType: SpanObjectTypeV3; - parentObjectId: LazyValue; - parentComputeObjectMetadataArgs: Record | undefined; - parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined; - propagatedEvent: StartSpanEventArgs | undefined; - propagatedState: PropagatedState | undefined; -} { - let argParentObjectId: LazyValue | undefined = undefined; - let argParentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined = - undefined; - let argPropagatedEvent: StartSpanEventArgs | undefined = undefined; - let argPropagatedState: PropagatedState | undefined = undefined; - const { parentSlug, propagatedState: parentPropagatedState } = - normalizeParent(args.parent, args.state); - if (parentSlug) { - if (args.parentSpanIds) { - throw new Error("Cannot specify both parent and parentSpanIds"); - } - const parentComponents = SpanComponentsV4.fromStr(parentSlug); - if (args.parentObjectType !== parentComponents.data.object_type) { - throw new Error( - `Mismatch between expected span parent object type ${args.parentObjectType} and provided type ${parentComponents.data.object_type}`, - ); - } - - // For logger/span method calls that receive a W3C parent, the inbound - // context links this span to the upstream trace ids, but the span still - // belongs to the current Braintrust project/experiment and should propagate - // that current parent onward. Keep the receiver's object id/metadata and - // only forward opaque W3C state (trace-flags/tracestate), not the inbound - // braintrust.parent fallback. - argParentObjectId = args.parentObjectId; - if ( - parentComponents.data.row_id && - parentSpanIdsUsable( - parentComponents.data.span_id, - parentComponents.data.root_span_id, - ) - ) { - argParentSpanIds = { - spanId: parentComponents.data.span_id, - rootSpanId: parentComponents.data.root_span_id, - }; - } - argPropagatedEvent = - args.propagatedEvent ?? - ((parentComponents.data.propagated_event ?? undefined) as - | StartSpanEventArgs - | undefined); - const propagatedState = args.propagatedState ?? parentPropagatedState; - if (propagatedState) { - const { braintrustParent: _ignoredBraintrustParent, ...w3cState } = - propagatedState; - argPropagatedState = w3cState; - } - } else { - argParentObjectId = args.parentObjectId; - argParentSpanIds = args.parentSpanIds; - argPropagatedEvent = args.propagatedEvent; - argPropagatedState = args.propagatedState; - } - - return { - parentObjectType: args.parentObjectType, - parentObjectId: argParentObjectId, - parentComputeObjectMetadataArgs: args.parentComputeObjectMetadataArgs, - parentSpanIds: argParentSpanIds, - propagatedEvent: argPropagatedEvent, - propagatedState: argPropagatedState, - }; -} - -export class Logger implements Exportable { - private state: BraintrustState; - private lazyMetadata: LazyValue; - private computeMetadataArgs: ProjectMetadataArgs | undefined; - private linkArgs: LinkArgs | undefined; - private lastStartTime: number; - private lazyId: LazyValue; - private calledStartSpan: boolean; - private readonly captureAttachments: boolean | undefined; - - // For type identification. - public kind = "logger" as const; - - constructor( - state: BraintrustState, - lazyMetadata: LazyValue, - options: { - computeMetadataArgs?: ProjectMetadataArgs; - linkArgs?: LinkArgs; - captureAttachments?: boolean; - } = {}, - ) { - this.lazyMetadata = lazyMetadata; - this.computeMetadataArgs = options.computeMetadataArgs; - this.linkArgs = options.linkArgs; - this.lastStartTime = getCurrentUnixTimestamp(); - this.lazyId = new LazyValue(() => this.id); - this.calledStartSpan = false; - this.captureAttachments = options.captureAttachments; - this.state = state; - } - - public get org_id(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).org_id; - })(); - } - - public get project(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).project; - })(); - } - - public get id(): Promise { - return (async () => (await this.project).id)(); - } - - public get loggingState(): BraintrustState { - return this.state; - } - - /** @internal */ - public get [CAPTURE_ATTACHMENTS](): boolean { - return this.captureAttachments ?? captureAttachmentsFromEnv(); - } - - private parentObjectType() { - return SpanObjectTypeV3.PROJECT_LOGS; - } - - /** - * Log a single event. The event will be batched and uploaded behind the scenes. Call and await {@link Logger.flush} to ensure the event has been uploaded. - * - * @param event The event to log. - * @param event.input: (Optional) the arguments that uniquely define a user input (an arbitrary, JSON serializable object). - * @param event.output: (Optional) the output of your application, including post-processing (an arbitrary, JSON serializable object), that allows you to determine whether the result is correct or not. For example, in an app that generates SQL queries, the `output` should be the _result_ of the SQL query generated by the model, not the query itself, because there may be multiple valid queries that answer a single question. - * @param event.expected: (Optional) the ground truth value (an arbitrary, JSON serializable object) that you'd compare to `output` to determine if your `output` value is correct or not. Braintrust currently does not compare `output` to `expected` for you, since there are so many different ways to do that correctly. Instead, these values are just used to help you navigate while digging into analyses. However, we may later use these values to re-score outputs or fine-tune your models. - * @param event.error: (Optional) The error that occurred, if any. If you use tracing to run an experiment, errors are automatically logged when your code throws an exception. - * @param event.scores: (Optional) a dictionary of numeric values (between 0 and 1) to log. The scores should give you a variety of signals that help you determine how accurate the outputs are compared to what you expect and diagnose failures. For example, a summarization app might have one score that tells you how accurate the summary is, and another that measures the word similarity between the generated and grouth truth summary. The word similarity score could help you determine whether the summarization was covering similar concepts or not. You can use these scores to help you sort, filter, and compare logs. - * @param event.metadata: (Optional) a dictionary with additional data about the test example, model outputs, or just about anything else that's relevant, that you can use to help find and analyze examples later. For example, you could log the `prompt`, example's `id`, or anything else that would be useful to slice/dice later. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. - * @param event.metrics: (Optional) a dictionary of metrics to log. The following keys are populated automatically: "start", "end". - * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you. - * @param options Additional logging options - * @param options.allowConcurrentWithSpans in rare cases where you need to log at the top level separately from spans on the logger elsewhere, set this to true. - * @returns The `id` of the logged event. - */ - public log( - event: Readonly, - options?: { allowConcurrentWithSpans?: boolean }, - ): string { - if (this.calledStartSpan && !options?.allowConcurrentWithSpans) { - throw new Error( - "Cannot run toplevel `log` method while using spans. To log to the span, call `logger.traced` and then log with `span.log`", - ); - } - - const span = this.startSpanImpl({ startTime: this.lastStartTime, event }); - this.lastStartTime = span.end(); - return span.id; - } - - /** - * Create a new toplevel span underneath the logger. The name defaults to "root". - * - * See {@link Span.traced} for full details. - */ - public traced( - callback: (span: Span) => R, - args?: StartSpanArgs & SetCurrentArg, - ): R { - const { setCurrent, ...argsRest } = args ?? {}; - const span = this.startSpan(argsRest); - - return runCatchFinally( - () => { - if (setCurrent ?? true) { - return withCurrent(span, callback); - } else { - return callback(span); - } - }, - (error) => { - logError(span, error); - throw error; - }, - () => span.end(), - ); - } - - /** - * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations - * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", - * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead. - * - * See {@link traced} for full details. - */ - public startSpan(args?: StartSpanArgs): Span { - this.calledStartSpan = true; - return this.startSpanImpl(args); - } - - private startSpanImpl(args?: StartSpanArgs): Span { - return new SpanImpl({ - [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], - ...args, - // Sometimes `args` gets passed directly into this function, and it contains an undefined value for `state`. - // To ensure that we always use this logger's state, we override the `state` argument no matter what. - state: this.state, - ...startSpanParentArgs({ - state: this.state, - parent: args?.parent, - parentObjectType: this.parentObjectType(), - parentObjectId: this.lazyId, - parentComputeObjectMetadataArgs: this.computeMetadataArgs, - parentSpanIds: args?.parentSpanIds, - propagatedEvent: args?.propagatedEvent, - }), - defaultRootType: SpanTypeAttribute.TASK, - }); - } - - /** - * Log feedback to an event. Feedback is used to save feedback scores, set an expected value, or add a comment. - * - * @param event - * @param event.id The id of the event to log feedback for. This is the `id` returned by `log` or accessible as the `id` field of a span. - * @param event.scores (Optional) a dictionary of numeric values (between 0 and 1) to log. These scores will be merged into the existing scores for the event. - * @param event.expected (Optional) the ground truth value (an arbitrary, JSON serializable object) that you'd compare to `output` to determine if your `output` value is correct or not. - * @param event.comment (Optional) an optional comment string to log about the event. - * @param event.metadata (Optional) a dictionary with additional data about the feedback. If you have a `user_id`, you can log it here and access it in the Braintrust UI. Note, this metadata does not correspond to the main event itself, but rather the audit log attached to the event. - * @param event.source (Optional) the source of the feedback. Must be one of "external" (default), "app", or "api". - */ - public logFeedback(event: LogFeedbackFullArgs): void { - logFeedbackImpl(this.state, this.parentObjectType(), this.lazyId, event); - } - - /** - * Update a span in the experiment using its id. It is important that you only update a span once the original span has been fully written and flushed, - * since otherwise updates to the span may conflict with the original span. - * - * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields. - */ - public updateSpan( - event: Omit, "id"> & - Required>, - ): void { - const { id, root_span_id, span_id, ...eventRest } = event; - if (!id) { - throw new Error("Span id is required to update a span"); - } - updateSpanImpl({ - state: this.state, - parentObjectType: this.parentObjectType(), - parentObjectId: this.lazyId, - id, - root_span_id, - span_id, - event: eventRest, - }); - } - - /** - * Return a serialized representation of the logger that can be used to start subspans in other places. - * - * See {@link Span.startSpan} for more details. - */ - public async export(): Promise { - // Note: it is important that the object id we are checking for - // `has_computed` is the same as the one we are passing into the span - // logging functions. So that if the spans actually do get logged, then this - // `_lazy_id` object specifically will also be marked as computed. - return new (getSpanComponentsClass())({ - object_type: this.parentObjectType(), - ...(this.computeMetadataArgs && !this.lazyId.hasSucceeded - ? { compute_object_metadata_args: this.computeMetadataArgs } - : { object_id: await this.lazyId.get() }), - }).toStr(); - } - - /* - * Flush any pending logs to the server. - */ - async flush(): Promise { - return await this.state.bgLogger().flush(); - } - - /** - * Return the base URL for links (e.g. https://braintrust.dev/app/my-org-name) - * if we have the info, otherwise return null. - * Resolution order: state -> linkArgs -> env var - */ - public _getLinkBaseUrl(): string | null { - return _getLinkBaseUrl(this.state, this.linkArgs); - } - - /** - * Return this logger's Braintrust parent string (`project_id:` or - * `project_name:`) for the `braintrust.parent` baggage entry, or - * undefined when it cannot be determined synchronously. - */ - public _getOtelParent(): string | undefined { - const id = - this.computeMetadataArgs?.project_id || this.lazyId.getSync().value; - if (id) { - return `project_id:${id}`; - } - const name = this.computeMetadataArgs?.project_name; - if (name) { - return `project_name:${name}`; - } - return undefined; - } -} - -export type Logs3OverflowUpload = { - method: "PUT" | "POST"; - signedUrl: string; - headers?: Record; - fields?: Record; - key: string; -}; - -export const logs3OverflowUploadSchema: z.ZodType = - z.object({ - method: z.enum(["PUT", "POST"]), - signedUrl: z.string().url(), - headers: z.record(z.string()).optional(), - fields: z.record(z.string()).optional(), - key: z.string().min(1), - }); - -export type Logs3OverflowInputRow = { - object_ids: Record; - is_delete?: boolean; - input_row: { - byte_size: number; - }; -}; - -type LogItemWithMeta = { - str: string; - overflowMeta: Logs3OverflowInputRow; -}; - -function constructLogs3Data(items: LogItemWithMeta[]) { - return `{"rows": ${constructJsonArray(items.map((i) => i.str))}, "api_version": 2}`; -} - -export function constructLogs3OverflowRequest(key: string) { - return { - rows: { - type: LOGS3_OVERFLOW_REFERENCE_TYPE, - key, - }, - api_version: 2, - }; -} - -export function pickLogs3OverflowObjectIds( - row: Record, -): Record { - const objectIds: Record = {}; - for (const key of OBJECT_ID_KEYS) { - if (key in row) { - objectIds[key] = row[key]; - } - } - return objectIds; -} - -/** - * Upload a logs3 overflow payload to the signed URL. - * This is a standalone function that can be used by both SDK and app code. - * - * @param upload - The overflow upload metadata from the API - * @param payload - The JSON payload string to upload - * @param fetchFn - Optional custom fetch function (defaults to global fetch) - */ -export async function uploadLogs3OverflowPayload( - upload: Logs3OverflowUpload, - payload: string, - fetchFn: typeof fetch = fetch, -): Promise { - if (upload.method === "POST") { - if (!upload.fields) { - throw new Error("Missing logs3 overflow upload fields"); - } - if (typeof FormData === "undefined" || typeof Blob === "undefined") { - throw new Error("FormData is not available for logs3 overflow upload"); - } - const form = new FormData(); - for (const [key, value] of Object.entries(upload.fields)) { - form.append(key, value); - } - const contentType = upload.fields["Content-Type"] ?? "application/json"; - form.append("file", new Blob([payload], { type: contentType })); - const headers: Record = {}; - for (const [key, value] of Object.entries(upload.headers ?? {})) { - if (key.toLowerCase() !== "content-type") { - headers[key] = value; - } - } - const response = await fetchFn(upload.signedUrl, { - method: "POST" satisfies RequestInit["method"], - headers, - body: form, - }); - if (!response.ok) { - const responseText = await response.text().catch(() => ""); - throw new Error( - `Failed to upload logs3 overflow payload: ${response.status} ${responseText}`, - ); - } - return; - } - const headers: Record = { ...(upload.headers ?? {}) }; - addAzureBlobHeaders(headers, upload.signedUrl); - const response = await fetchFn(upload.signedUrl, { - method: "PUT" satisfies RequestInit["method"], - headers, - body: payload, - }); - if (!response.ok) { - const responseText = await response.text().catch(() => ""); - throw new Error( - `Failed to upload logs3 overflow payload: ${response.status} ${responseText}`, - ); - } -} - -function stringifyWithOverflowMeta(item: object): LogItemWithMeta { - const str = JSON.stringify(item); - const record = item as Record; - return { - str, - overflowMeta: { - object_ids: pickLogs3OverflowObjectIds(record), - is_delete: record[OBJECT_DELETE_FIELD] === true, - input_row: { - byte_size: utf8ByteLength(str), - }, - }, - }; -} - -export function utf8ByteLength(value: string) { - if (typeof TextEncoder !== "undefined") { - return new TextEncoder().encode(value).length; - } - return value.length; -} - -function now() { - return new Date().getTime(); -} - -export interface BackgroundLoggerOpts { - noExitFlush?: boolean; - onFlushError?: (error: unknown) => void; -} - -const DEFAULT_FLUSH_BACKPRESSURE_BYTES = 10 * 1024 * 1024; // 10 MB -const DEFAULT_MAX_CONCURRENT_LOG_REQUESTS = 8; - -// Read a numeric environment variable. Unset, empty, whitespace-only and -// non-numeric values return undefined so the caller keeps its default, rather -// than being read as 0 (`Number("") === 0`). -function getNumericEnv(name: string): number | undefined { - const raw = iso.getEnv(name); - if (raw === undefined || raw === null || raw.trim() === "") { - return undefined; - } - const value = Number(raw); - return isNaN(value) ? undefined : value; -} - -interface BackgroundLogger { - log(items: LazyValue[]): void; - flush(): Promise; - pendingFlushBytes(): number; - flushBackpressureBytes(): number; - setMaskingFunction( - maskingFunction: ((value: unknown) => unknown) | null, - ): void; -} - -export class TestBackgroundLogger implements BackgroundLogger { - private items: LazyValue[][] = []; - private exportCustomizers: readonly SpanCustomizer[] = []; - - log(items: LazyValue[]): void { - this.items.push(items); - } - - setMaskingFunction( - maskingFunction: ((value: unknown) => unknown) | null, - ): void { - this.exportCustomizers = maskingFunction - ? [createMaskingCustomizer(maskingFunction)] - : []; - } - - async flush(): Promise { - return Promise.resolve(); - } - - pendingFlushBytes(): number { - return 0; - } - - flushBackpressureBytes(): number { - return DEFAULT_FLUSH_BACKPRESSURE_BYTES; - } - - async drain(): Promise { - const items = this.items; - this.items = []; - - // get all the values - const events: BackgroundLogEvent[] = []; - for (const item of items) { - for (const event of item) { - const value = await event.get(); - if (value !== null) events.push(value); - } - } - - let batch = mergeRowBatch(events); - - if (this.exportCustomizers.length) { - batch = batch - .map( - (item) => - customizeSpanExport( - item as SpanExportData, - this.exportCustomizers, - ) as BackgroundLogEvent | null, - ) - .filter((item): item is BackgroundLogEvent => item !== null); - } - - return batch; - } -} - -const BACKGROUND_LOGGER_BASE_SLEEP_TIME_S = 1.0; -const HTTP_RETRY_BASE_SLEEP_TIME_S = 1.0; -const HTTP_RETRY_JITTER_MS = 200; -const BTQL_HTTP_RETRIES = 3; -const RETRYABLE_HTTP_STATUS_CODES = new Set([500, 502, 503, 504]); - -function isRetryableHTTPError(error: unknown): boolean { - return ( - !(error instanceof FailedHTTPResponse) || - RETRYABLE_HTTP_STATUS_CODES.has(error.status) - ); -} - -function formatHTTPError(error: unknown): string { - if (error instanceof FailedHTTPResponse) { - return `${error.status} ${error.text}`; - } - return error instanceof Error ? error.message : String(error); -} - -function getAbortReason(signal: AbortSignal): unknown { - return signal.reason ?? new Error("Request aborted"); -} - -async function waitForRetry( - delayMs: number, - signal?: AbortSignal | null, -): Promise { - if (!signal) { - await new Promise((resolve) => setTimeout(resolve, delayMs)); - return; - } - - if (signal.aborted) { - throw getAbortReason(signal); - } - - await new Promise((resolve, reject) => { - const onAbort = () => { - clearTimeout(timeout); - signal.removeEventListener("abort", onAbort); - reject(getAbortReason(signal)); - }; - const timeout = setTimeout(() => { - signal.removeEventListener("abort", onAbort); - resolve(); - }, delayMs); - signal.addEventListener("abort", onAbort, { once: true }); - if (signal.aborted) { - onAbort(); - } - }); -} - -// We should only have one instance of this object per state object in -// 'BraintrustState._bgLogger'. Be careful about spawning multiple -// instances of this class, because concurrent BackgroundLoggers will not log to -// the backend in a deterministic order. -class ConcurrencyLimiter { - private activeCount = 0; - private readonly waiters: Array<() => void> = []; - - constructor(private readonly limit: number) {} - - async run(callback: () => Promise): Promise { - if (this.activeCount >= this.limit) { - await new Promise((resolve) => this.waiters.push(resolve)); - } else { - this.activeCount++; - } - - try { - return await callback(); - } finally { - const next = this.waiters.shift(); - if (next) { - next(); - } else { - this.activeCount--; - } - } - } -} - -interface QueuedLogEvent { - sequence: number; - event: LazyValue; -} - -class HTTPBackgroundLogger implements BackgroundLogger { - private apiConn: LazyValue; - private queue: Queue; - private activeFlush: Promise = Promise.resolve(); - private activeFlushResolved = true; - private onFlushError?: (error: unknown) => void; - private exportCustomizers: readonly SpanCustomizer[] = []; - private readonly requestLimiter: ConcurrencyLimiter; - private lastEnqueuedSequence = 0; - private completedSequence = 0; - - public syncFlush: boolean = false; - private maxRequestSizeOverride: number | null = null; - private _maxRequestSizePromise: Promise<{ - maxRequestSize: number; - canUseOverflow: boolean; - }> | null = null; - public defaultBatchSize: number = 100; - public numTries: number = 3; - public queueDropExceedingMaxsize: number = DEFAULT_QUEUE_SIZE; - public queueDropLoggingPeriod: number = 60; - public failedPublishPayloadsDir: string | undefined = undefined; - public allPublishPayloadsDir: string | undefined = undefined; - private _flushBackpressureBytes: number = DEFAULT_FLUSH_BACKPRESSURE_BYTES; - - private _pendingBytes: number = 0; - - private _disabled = false; - - private queueDropLoggingState = { - numDropped: 0, - lastLoggedTimestamp: 0, - }; - - constructor(apiConn: LazyValue, opts?: BackgroundLoggerOpts) { - opts = opts ?? {}; - this.apiConn = apiConn; - - const syncFlushEnv = getNumericEnv("BRAINTRUST_SYNC_FLUSH"); - if (syncFlushEnv !== undefined) { - this.syncFlush = Boolean(syncFlushEnv); - } - - const defaultBatchSizeEnv = getNumericEnv("BRAINTRUST_DEFAULT_BATCH_SIZE"); - if (defaultBatchSizeEnv !== undefined) { - this.defaultBatchSize = defaultBatchSizeEnv; - } - - const maxRequestSizeEnv = getNumericEnv("BRAINTRUST_MAX_REQUEST_SIZE"); - if (maxRequestSizeEnv !== undefined) { - this.maxRequestSizeOverride = maxRequestSizeEnv; - } - - const numTriesEnv = getNumericEnv("BRAINTRUST_NUM_RETRIES"); - if (numTriesEnv !== undefined) { - this.numTries = numTriesEnv + 1; - } - - const queueDropExceedingMaxsizeEnv = getNumericEnv( - "BRAINTRUST_QUEUE_DROP_EXCEEDING_MAXSIZE", - ); - - if (queueDropExceedingMaxsizeEnv !== undefined) { - this.queueDropExceedingMaxsize = queueDropExceedingMaxsizeEnv; - } - - this.queue = new Queue(this.queueDropExceedingMaxsize); - - const maxConcurrentLogRequests = - getNumericEnv("BRAINTRUST_MAX_CONCURRENT_LOG_REQUESTS") ?? - DEFAULT_MAX_CONCURRENT_LOG_REQUESTS; - if ( - !Number.isInteger(maxConcurrentLogRequests) || - maxConcurrentLogRequests < 1 - ) { - debugLogger.warn( - `maxConcurrentLogRequests ${maxConcurrentLogRequests} is not a positive integer, using default ${DEFAULT_MAX_CONCURRENT_LOG_REQUESTS}`, - ); - this.requestLimiter = new ConcurrencyLimiter( - DEFAULT_MAX_CONCURRENT_LOG_REQUESTS, - ); - } else { - this.requestLimiter = new ConcurrencyLimiter(maxConcurrentLogRequests); - } - - const queueDropLoggingPeriodEnv = getNumericEnv( - "BRAINTRUST_QUEUE_DROP_LOGGING_PERIOD", - ); - if (queueDropLoggingPeriodEnv !== undefined) { - this.queueDropLoggingPeriod = queueDropLoggingPeriodEnv; - } - - if (iso.getEnv("BRAINTRUST_LOG_FLUSH_CHUNK_SIZE")) { - debugLogger.warn( - "BRAINTRUST_LOG_FLUSH_CHUNK_SIZE is deprecated and no longer has any effect. " + - "Log flushing now sends all items at once and batches them automatically. " + - "This environment variable will be removed in a future major release.", - ); - } - - const flushBackpressureBytesEnv = getNumericEnv( - "BRAINTRUST_FLUSH_BACKPRESSURE_BYTES", - ); - if ( - flushBackpressureBytesEnv !== undefined && - flushBackpressureBytesEnv > 0 - ) { - this._flushBackpressureBytes = flushBackpressureBytesEnv; - } - - const failedPublishPayloadsDirEnv = iso.getEnv( - "BRAINTRUST_FAILED_PUBLISH_PAYLOADS_DIR", - ); - if (failedPublishPayloadsDirEnv) { - this.failedPublishPayloadsDir = failedPublishPayloadsDirEnv; - } - - const allPublishPayloadsDirEnv = iso.getEnv( - "BRAINTRUST_ALL_PUBLISH_PAYLOADS_DIR", - ); - if (allPublishPayloadsDirEnv) { - this.allPublishPayloadsDir = allPublishPayloadsDirEnv; - } - - // Note that this will not run for explicit termination events, such as - // calls to `process.exit()` or uncaught exceptions. Thus it is a - // "best-effort" flush. - if (!opts.noExitFlush) { - iso.processOn("beforeExit", async () => { - await this.flush(); - }); - } - this.onFlushError = opts.onFlushError; - } - - setMaskingFunction( - maskingFunction: ((value: unknown) => unknown) | null, - ): void { - this.exportCustomizers = maskingFunction - ? [createMaskingCustomizer(maskingFunction)] - : []; - } - - pendingFlushBytes(): number { - return this._pendingBytes; - } - - flushBackpressureBytes(): number { - return this._flushBackpressureBytes; - } - - log(items: LazyValue[]) { - if (this._disabled) { - return; - } - - const queuedItems = items.map((event) => ({ - sequence: ++this.lastEnqueuedSequence, - event, - })); - const droppedItems = this.queue.push(...queuedItems); - // Queue limits reject a suffix of newly pushed items, so their sequence - // numbers can be reused by the next accepted events. - this.lastEnqueuedSequence -= droppedItems.length; - - if (!this.syncFlush) { - this.triggerActiveFlush(); - } - - if (droppedItems.length) { - this.registerDroppedItemCount(droppedItems.length); - if (this.allPublishPayloadsDir || this.failedPublishPayloadsDir) { - this.dumpDroppedEvents(droppedItems.map((item) => item.event)); - } - } - } - - private getMaxRequestSize(): Promise<{ - maxRequestSize: number; - canUseOverflow: boolean; - }> { - if (!this._maxRequestSizePromise) { - this._maxRequestSizePromise = (async () => { - let serverLimit: number | null = null; - try { - const conn = await this.apiConn.get(); - const versionInfo = await conn.get_json("version"); - serverLimit = - z - .object({ logs3_payload_max_bytes: z.number().nullish() }) - .parse(versionInfo).logs3_payload_max_bytes ?? null; - } catch (e) { - debugLogger.warn( - "Failed to fetch version info for payload limit:", - e, - ); - } - const validServerLimit = - serverLimit !== null && serverLimit > 0 ? serverLimit : null; - const canUseOverflow = validServerLimit !== null; - let maxRequestSize = DEFAULT_MAX_REQUEST_SIZE; - if (this.maxRequestSizeOverride !== null) { - maxRequestSize = - validServerLimit !== null - ? Math.min(this.maxRequestSizeOverride, validServerLimit) - : this.maxRequestSizeOverride; - } else if (validServerLimit !== null) { - maxRequestSize = validServerLimit; - } - return { maxRequestSize, canUseOverflow }; - })(); - } - return this._maxRequestSizePromise; - } - - async flush(): Promise { - const targetSequence = this.lastEnqueuedSequence; - - while (this.completedSequence < targetSequence) { - this.triggerActiveFlush(); - await this.activeFlush; - } - } - - private async flushThrough( - flushBoundary: number, - args?: { batchSize?: number }, - ): Promise { - const batchSize = args?.batchSize ?? this.defaultBatchSize; - - const queuedItems = this.queue.drainWhile( - (item) => item.sequence <= flushBoundary, - ); - if (queuedItems.length === 0) { - return; - } - - try { - if (!this._disabled) { - await this.flushWrappedItemsChunk( - queuedItems.map((item) => item.event), - batchSize, - ); - } - } catch (error) { - this.reportFlushError(error); - } finally { - this.completedSequence = queuedItems[queuedItems.length - 1].sequence; - } - } - - private async flushWrappedItemsChunk( - wrappedItems: LazyValue[], - batchSize: number, - ) { - if (!wrappedItems.length) { - return; - } - - const [allItems, attachments] = await this.unwrapLazyValues(wrappedItems); - if (allItems.length === 0) { - return; - } - - // Construct batches of records to flush in parallel. - let chunkBytes = 0; - const allItemsWithMeta = allItems.map((item) => { - const withMeta = stringifyWithOverflowMeta(item); - chunkBytes += withMeta.str.length; - return withMeta; - }); - this._pendingBytes += chunkBytes; - - const maxRequestSizeResult = await this.getMaxRequestSize(); - const batches = batchItems({ - items: allItemsWithMeta, - batchMaxNumItems: batchSize, - batchMaxNumBytes: maxRequestSizeResult.maxRequestSize / 2, - getByteSize: (item) => item.str.length, - }); - - const postPromises = batches.map((batch) => - this.requestLimiter.run(async () => { - try { - await this.submitLogsRequest(batch, maxRequestSizeResult); - return { type: "success" } as const; - } catch (e) { - return { type: "error", value: e } as const; - } - }), - ); - const results = await Promise.all(postPromises); - this._pendingBytes = Math.max(0, this._pendingBytes - chunkBytes); - const failingResultErrors = results - .map((r) => (r.type === "success" ? undefined : r.value)) - .filter((r) => r !== undefined); - if (failingResultErrors.length) { - throw new AggregateError( - failingResultErrors, - `Encountered the following errors while logging:`, - ); - } - - const attachmentErrors: unknown[] = []; - // For now, upload attachments serially. - for (const attachment of attachments) { - try { - const result = await attachment.upload(); - if (result.upload_status === "error") { - throw new Error(result.error_message); - } - } catch (error) { - attachmentErrors.push(error); - } - } - if (attachmentErrors.length === 1) { - throw attachmentErrors[0]; - } else if (attachmentErrors.length > 1) { - throw new AggregateError( - attachmentErrors, - `Encountered the following errors while uploading attachments:`, - ); - } - } - - private async unwrapLazyValues( - wrappedItems: LazyValue[], - ): Promise<[BackgroundLogEvent[], Attachment[]]> { - for (let i = 0; i < this.numTries; ++i) { - try { - const items = ( - await Promise.all(wrappedItems.map((x) => x.get())) - ).filter((item): item is BackgroundLogEvent => item !== null); - - // TODO(kevin): `extractAttachments` should ideally come after - // `mergeRowBatch`, since merge-overwriting could result in some - // attachments that no longer need to be uploaded. This would require - // modifying the merge logic to treat the Attachment object as a "leaf" - // rather than attempting to merge the keys of the Attachment. - const attachments: Attachment[] = []; - items.forEach((item) => extractAttachments(item, attachments)); - - let mergedItems = mergeRowBatch(items); - - if (this.exportCustomizers.length) { - mergedItems = mergedItems - .map( - (item) => - customizeSpanExport( - item as SpanExportData, - this.exportCustomizers, - ) as BackgroundLogEvent | null, - ) - .filter((item): item is BackgroundLogEvent => item !== null); - } - - return [mergedItems, attachments]; - } catch (e) { - let errmsg = "Encountered error when constructing records to flush"; - const isRetrying = i + 1 < this.numTries; - if (isRetrying) { - errmsg += ". Retrying"; - } - - debugLogger.warn(errmsg); - if (!isRetrying) { - debugLogger.warn( - `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`, - ); - throw e; - } else { - debugLogger.warn(e); - const sleepTimeS = BACKGROUND_LOGGER_BASE_SLEEP_TIME_S * 2 ** i; - debugLogger.info(`Sleeping for ${sleepTimeS}s`); - await new Promise((resolve) => - setTimeout(resolve, sleepTimeS * 1000), - ); - } - } - } - throw new Error("Impossible"); - } - - private async requestLogs3OverflowUpload( - conn: HTTPConnection, - args: { rows: Logs3OverflowInputRow[]; sizeBytes: number }, - ): Promise { - let response: unknown; - try { - response = await conn.post_json("logs3/overflow", { - content_type: "application/json", - size_bytes: args.sizeBytes, - rows: args.rows, - }); - } catch (error) { - const errorStr = JSON.stringify(error); - throw new Error( - `Failed to request logs3 overflow upload URL: ${errorStr}`, - ); - } - - try { - return logs3OverflowUploadSchema.parse(response); - } catch (error) { - if (error instanceof ZodError) { - const errorStr = JSON.stringify(error.flatten()); - throw new Error(`Invalid response from API server: ${errorStr}`); - } - throw error; - } - } - - private async _uploadLogs3OverflowPayload( - conn: HTTPConnection, - upload: Logs3OverflowUpload, - payload: string, - ): Promise { - // Delegate to the public function, using the connection's fetch - await uploadLogs3OverflowPayload(upload, payload, conn.fetch.bind(conn)); - } - - private async submitLogsRequest( - items: LogItemWithMeta[], - { - maxRequestSize, - canUseOverflow, - }: { maxRequestSize: number; canUseOverflow: boolean }, - ): Promise { - const conn = await this.apiConn.get(); - const dataStr = constructLogs3Data(items); - const payloadBytes = utf8ByteLength(dataStr); - const useOverflow = canUseOverflow && payloadBytes > maxRequestSize; - if (this.allPublishPayloadsDir) { - await HTTPBackgroundLogger.writePayloadToDir({ - payloadDir: this.allPublishPayloadsDir, - payload: dataStr, - }); - } - - let overflowUpload: Logs3OverflowUpload | null = null; - const overflowRows = useOverflow - ? items.map((item) => item.overflowMeta) - : null; - - for (let i = 0; i < this.numTries; i++) { - const startTime = now(); - let error: unknown = undefined; - try { - if (overflowRows) { - if (!overflowUpload) { - const currentUpload = await this.requestLogs3OverflowUpload(conn, { - rows: overflowRows, - sizeBytes: payloadBytes, - }); - await this._uploadLogs3OverflowPayload( - conn, - currentUpload, - dataStr, - ); - overflowUpload = currentUpload; - } - await conn.post_json( - "logs3", - constructLogs3OverflowRequest(overflowUpload.key), - ); - } else { - await conn.post_json("logs3", dataStr); - } - } catch (e) { - error = e; - } - if (error === undefined) { - return; - } - - const isRetrying = i + 1 < this.numTries; - const retryingText = isRetrying ? "" : " Retrying"; - const errorText = (() => { - if (error instanceof FailedHTTPResponse) { - return `${error.status} (${error.text}): ${error.data}`; - } else { - return `${error}`; - } - })(); - const errMsg = `log request failed. Elapsed time: ${ - (now() - startTime) / 1000 - } seconds. Payload size: ${ - payloadBytes - }.${retryingText}\nError: ${errorText}`; - - if (!isRetrying && this.failedPublishPayloadsDir) { - await HTTPBackgroundLogger.writePayloadToDir({ - payloadDir: this.failedPublishPayloadsDir, - payload: dataStr, - }); - this.logFailedPayloadsDir(); - } - - if (!isRetrying) { - debugLogger.warn( - `log request failed after ${this.numTries} retries. Dropping batch`, - ); - throw new Error(errMsg); - } else { - debugLogger.warn(errMsg); - if (isRetrying) { - const sleepTimeS = BACKGROUND_LOGGER_BASE_SLEEP_TIME_S * 2 ** i; - debugLogger.info(`Sleeping for ${sleepTimeS}s`); - await new Promise((resolve) => - setTimeout(resolve, sleepTimeS * 1000), - ); - } - } - } - } - - private registerDroppedItemCount(numItems: number) { - if (numItems <= 0) { - return; - } - this.queueDropLoggingState.numDropped += numItems; - const timeNow = getCurrentUnixTimestamp(); - if ( - timeNow - this.queueDropLoggingState.lastLoggedTimestamp > - this.queueDropLoggingPeriod - ) { - debugLogger.warn( - `Dropped ${this.queueDropLoggingState.numDropped} elements due to full queue`, - ); - if (this.failedPublishPayloadsDir) { - this.logFailedPayloadsDir(); - } - this.queueDropLoggingState.numDropped = 0; - this.queueDropLoggingState.lastLoggedTimestamp = timeNow; - } - } - - private async dumpDroppedEvents( - wrappedItems: LazyValue[], - ) { - const publishPayloadsDir = [ - this.allPublishPayloadsDir, - this.failedPublishPayloadsDir, - ].reduce((acc, x) => (x ? acc.concat([x]) : acc), new Array()); - if (!(wrappedItems.length && publishPayloadsDir.length)) { - return; - } - try { - const [allItems, allAttachments] = - await this.unwrapLazyValues(wrappedItems); - - const dataStr = constructLogs3Data( - allItems.map((x) => stringifyWithOverflowMeta(x)), - ); - const attachmentStr = JSON.stringify( - allAttachments.map((a) => a.debugInfo()), - ); - - const payload = `{"data": ${dataStr}, "attachments": ${attachmentStr}}\n`; - - for (const payloadDir of publishPayloadsDir) { - await HTTPBackgroundLogger.writePayloadToDir({ payloadDir, payload }); - } - } catch (e) { - debugLogger.error(e); - } - } - - private static async writePayloadToDir({ - payloadDir, - payload, - }: { - payloadDir: string; - payload: string; - }) { - if (!(iso.pathJoin && iso.mkdir && iso.writeFile)) { - debugLogger.warn( - "Cannot dump payloads: filesystem-operations not supported on this platform", - ); - return; - } - const payloadFile = iso.pathJoin( - payloadDir, - `payload_${getCurrentUnixTimestamp()}_${uuidv4().slice(0, 8)}.json`, - ); - try { - await iso.mkdir(payloadDir, { recursive: true }); - await iso.writeFile(payloadFile, payload); - } catch (e) { - debugLogger.error( - `Failed to write failed payload to output file ${payloadFile}:\n`, - e, - ); - } - } - - private triggerActiveFlush() { - if (this.activeFlushResolved) { - const flushBoundary = this.lastEnqueuedSequence; - if (flushBoundary <= this.completedSequence) { - return; - } - - this.activeFlushResolved = false; - this.activeFlush = (async () => { - try { - await this.flushThrough(flushBoundary); - } catch (err) { - this.reportFlushError(err); - } finally { - this.activeFlushResolved = true; - if (!this.syncFlush && this.queue.length() > 0) { - this.triggerActiveFlush(); - } - } - })(); - - try { - waitUntil(this.activeFlush); - } catch (error) { - debugLogger.error("Failed to register background flush", error); - } - } - } - - private reportFlushError(error: unknown) { - const errors = error instanceof AggregateError ? error.errors : [error]; - for (const currentError of errors) { - try { - this.onFlushError?.(currentError); - } catch (callbackError) { - debugLogger.error("Error in onFlushError callback", callbackError); - } - } - } - - private logFailedPayloadsDir() { - debugLogger.warn( - `Logging failed payloads to ${this.failedPublishPayloadsDir}`, - ); - } - - // Should only be called by BraintrustState. - public internalReplaceApiConn(apiConn: HTTPConnection) { - this.apiConn = new LazyValue(async () => apiConn); - } - - public disable() { - this._disabled = true; - this.triggerActiveFlush(); - } - - public enforceQueueSizeLimit(enforce: boolean) { - this.queue.enforceQueueSizeLimit(enforce); - } -} - -type InitOpenOption = { - open?: IsOpen; -}; - -/** - * Reference to a dataset by ID and optional explicit selector. - */ -type DatasetSelection = { - version?: string; - environment?: string; - snapshotName?: string; -}; - -type DatasetSnapshotNameLookup = { - snapshotName: string; - xactId?: never; -}; - -type DatasetSnapshotXactLookup = { - snapshotName?: never; - xactId: string; -}; - -export type DatasetSnapshotLookup = - | DatasetSnapshotNameLookup - | DatasetSnapshotXactLookup; - -function isDatasetSnapshotNameLookup( - lookup: DatasetSnapshotLookup, -): lookup is DatasetSnapshotNameLookup { - return "snapshotName" in lookup; -} - -function assertDatasetSnapshotLookup(lookup: { - snapshotName?: string; - xactId?: string; -}): asserts lookup is DatasetSnapshotLookup { - const hasSnapshotName = lookup.snapshotName !== undefined; - const hasXactId = lookup.xactId !== undefined; - if (hasSnapshotName === hasXactId) { - throw new Error("Exactly one of snapshotName or xactId must be provided"); - } -} - -type DatasetPinState = { - lazyPinnedVersion?: LazyValue; - pinnedEnvironment?: string; - pinnedSnapshotName?: string; -}; - -export type DatasetRef = { - id: string; -} & DatasetSelection; - -export interface ParametersRef { - id: string; - version?: string; -} - -export type InitOptions = FullLoginOptions & { - experiment?: string; - description?: string; - dataset?: AnyDataset | DatasetRef; - _internal_btql?: Record; - parameters?: ParametersRef | RemoteEvalParameters; - update?: boolean; - baseExperiment?: string; - isPublic?: boolean; - metadata?: Record; - tags?: string[]; - gitMetadataSettings?: GitMetadataSettings; - projectId?: string; - baseExperimentId?: string; - repoInfo?: RepoInfo; - setCurrent?: boolean; - state?: BraintrustState; -} & InitOpenOption; - -export type FullInitOptions = { - project?: string; -} & InitOptions; - -function getExperimentDatasetFilter({ - dataset, - _internal_btql, -}: { - dataset?: AnyDataset | DatasetRef; - _internal_btql?: Record; -}): Record | undefined { - if (_internal_btql !== undefined) { - return _internal_btql; - } - - if (!(dataset instanceof Dataset)) { - return undefined; - } - - const datasetFilter = Reflect.get(dataset, "_internal_btql"); - return isObject(datasetFilter) ? datasetFilter : undefined; -} - -function getInternalBtqlLimit( - internalBtql?: Record, -): number | undefined { - const limit = internalBtql?.["limit"]; - return typeof limit === "number" ? limit : undefined; -} - -type InitializedExperiment = - IsOpen extends true ? ReadonlyExperiment : Experiment; - -/** - * Log in, and then initialize a new experiment in a specified project. If the project does not exist, it will be created. - * - * @param options Options for configuring init(). - * @param options.project The name of the project to create the experiment in. Must specify at least one of `project` or `projectId`. - * @param options.experiment The name of the experiment to create. If not specified, a name will be generated automatically. - * @param options.description An optional description of the experiment. - * @param options.dataset (Optional) A dataset to associate with the experiment. You can pass in the name of the dataset (in the same project) or a dataset object (from any project). - * @param options.update If the experiment already exists, continue logging to it. If it does not exist, creates the experiment with the specified arguments. - * @param options.baseExperiment An optional experiment name to use as a base. If specified, the new experiment will be summarized and compared to this experiment. Otherwise, it will pick an experiment by finding the closest ancestor on the default (e.g. main) branch. - * @param options.isPublic An optional parameter to control whether the experiment is publicly visible to anybody with the link or privately visible to only members of the organization. Defaults to private. - * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. - * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. - * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. - * @param options.metadata (Optional) A dictionary with additional data about the test example, model outputs, or just about anything else that's relevant, that you can use to help find and analyze examples later. For example, you could log the `prompt`, example's `id`, or anything else that would be useful to slice/dice later. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. - * @param options.gitMetadataSettings (Optional) Settings for collecting git metadata. By default, Braintrust collects the git metadata fields allowed by your organization's git metadata settings. If those settings are absent, git metadata is not collected unless this option is set. - * @param setCurrent If true (the default), set the global current-experiment to the newly-created one. - * @param options.open If the experiment already exists, open it in read-only mode. Throws an error if the experiment does not already exist. - * @param options.projectId The id of the project to create the experiment in. This takes precedence over `project` if specified. - * @param options.baseExperimentId An optional experiment id to use as a base. If specified, the new experiment will be summarized and compared to this. This takes precedence over `baseExperiment` if specified. - * @param options.repoInfo (Optional) Explicitly specify the git metadata for this experiment. This takes precedence over `gitMetadataSettings` if specified. - * @param options.tags (Optional) A list of tags to attach to the experiment. - * @returns The newly created Experiment. - */ -export function init( - options: Readonly>, -): InitializedExperiment { - const { - project, - experiment, - description, - dataset, - _internal_btql, - parameters, - baseExperiment, - isPublic, - open, - update, - appUrl, - apiKey, - orgName, - forceLogin, - fetch, - metadata, - tags, - gitMetadataSettings, - projectId, - baseExperimentId, - repoInfo, - state: stateArg, - } = options; - - if (!project && !projectId) { - throw new Error("Must specify at least one of project or projectId"); - } - - if (open && update) { - throw new Error("Cannot open and update an experiment at the same time"); - } - - const state = stateArg ?? _globalState; - state._internalSetTraceContextSigningSecret(apiKey); - - // Ensure unlimited queue for init() calls (experiments) - // Experiments should never drop data - state.enforceQueueSizeLimit(false); - - if (open) { - if (isEmpty(experiment)) { - throw new Error(`Cannot open an experiment without specifying its name`); - } - - const lazyMetadata: LazyValue = new LazyValue( - async () => { - await state.login({ apiKey, appUrl, orgName, fetch, forceLogin }); - const args: Record = { - project_name: project, - project_id: projectId, - org_name: state.orgName, - experiment_name: experiment, - }; - - const response = await state - .appConn() - .post_json("api/experiment/get", args); - - if (response.length === 0) { - throw new Error( - `Experiment ${experiment} not found in project ${ - projectId ?? project - }.`, - ); - } - - const info = response[0]; - return { - project: { - id: info.project_id, - name: project ?? "UNKNOWN_PROJECT", - fullInfo: {}, - }, - experiment: { - id: info.id, - name: info.name, - fullInfo: info, - }, - }; - }, - ); - - return new ReadonlyExperiment( - stateArg ?? _globalState, - lazyMetadata, - ) as InitializedExperiment; - } - - const lazyMetadata: LazyValue = new LazyValue( - async () => { - await state.login({ apiKey, appUrl, orgName }); - const args: Record = { - project_name: project, - project_id: projectId, - org_id: state.orgId, - update, - }; - - if (experiment) { - args["experiment_name"] = experiment; - } - - if (description) { - args["description"] = description; - } - - const repoInfoArg = await (async (): Promise => { - if (repoInfo) { - return repoInfo; - } - const mergedGitMetadataSettings = - state.gitMetadataSettings == null - ? (gitMetadataSettings ?? { collect: "none" as const }) - : mergeGitMetadataSettings( - state.gitMetadataSettings, - gitMetadataSettings ?? { collect: "all" as const }, - ); - return await iso.getRepoInfo(mergedGitMetadataSettings); - })(); - - if (repoInfoArg) { - args["repo_info"] = repoInfoArg; - } - - if (baseExperimentId) { - args["base_exp_id"] = baseExperimentId; - } else if (baseExperiment) { - args["base_experiment"] = baseExperiment; - } else { - args["ancestor_commits"] = await iso.getPastNAncestors(); - } - - if (dataset !== undefined) { - const datasetSelection = await serializeDatasetForExperiment({ - dataset, - state, - }); - args["dataset_id"] = datasetSelection.datasetId; - if (datasetSelection.datasetVersion !== undefined) { - args["dataset_version"] = datasetSelection.datasetVersion; - } - } - - const datasetFilter = getExperimentDatasetFilter({ - dataset, - _internal_btql, - }); - if (datasetFilter !== undefined) { - args["internal_metadata"] = { - dataset_filter: datasetFilter, - }; - } - - if (parameters !== undefined) { - if (RemoteEvalParameters.isParameters(parameters)) { - args["parameters_id"] = parameters.id; - args["parameters_version"] = parameters.version; - } else { - args["parameters_id"] = parameters.id; - if (parameters.version !== undefined) { - args["parameters_version"] = parameters.version; - } - } - } - - if (isPublic !== undefined) { - args["public"] = isPublic; - } - - if (metadata) { - args["metadata"] = metadata; - } - - if (tags) { - validateTags(tags); - args["tags"] = tags; - } - - let response = null; - while (true) { - try { - response = await state - .appConn() - .post_json("api/experiment/register", args); - break; - } catch (e: any) { - if ( - args["base_experiment"] && - `${"data" in e && e.data}`.includes("base experiment") - ) { - debugLogger - .forState(state) - .warn(`Base experiment ${args["base_experiment"]} not found.`); - delete args["base_experiment"]; - } else { - throw e; - } - } - } - - return { - project: { - id: response.project.id, - name: response.project.name, - fullInfo: response.project, - }, - experiment: { - id: response.experiment.id, - name: response.experiment.name, - created: response.experiment.created, - fullInfo: response.experiment, - }, - }; - }, - ); - - const ret = new Experiment( - state, - lazyMetadata, - dataset !== undefined ? (dataset as AnyDataset) : undefined, - ); - if (options.setCurrent ?? true) { - state.currentExperiment = ret; - } - return ret as InitializedExperiment; -} - -declare global { - // Set by the bt eval runner when CLI-controlled BTQL should be pushed down - // into dataset-backed evals. - // eslint-disable-next-line no-var - var __bt_eval_internal_btql: Record | undefined; -} - -export type InitDatasetOptions = FullLoginOptions & { - dataset?: string; - datasetId?: string; - description?: string; - version?: string; - environment?: string; - snapshotName?: string; - projectId?: string; - metadata?: Record; - state?: BraintrustState; - _internal_btql?: Record; -}; - -export type FullInitDatasetOptions = { - project?: string; -} & InitDatasetOptions; - -async function getDatasetSnapshots( - params: - | { - state: BraintrustState; - datasetId: string; - } - | ({ - state: BraintrustState; - datasetId: string; - } & DatasetSnapshotLookup), -): Promise { - const { state, datasetId } = params; - return datasetSnapshotSchema.array().parse( - await state.appConn().post_json("api/dataset_snapshot/get", { - dataset_id: datasetId, - ...("snapshotName" in params ? { name: params.snapshotName } : {}), - ...("xactId" in params ? { xact_id: params.xactId } : {}), - }), - ); -} - -async function getDatasetSnapshot( - params: { - state: BraintrustState; - datasetId: string; - } & DatasetSnapshotLookup, -): Promise { - assertDatasetSnapshotLookup(params); - const snapshots = await getDatasetSnapshots(params); - if (snapshots.length > 1) { - throw new Error( - isDatasetSnapshotNameLookup(params) - ? `Expected a unique dataset snapshot named "${params.snapshotName}" for ${params.datasetId}` - : `Expected a unique dataset snapshot for xact_id "${params.xactId}" in ${params.datasetId}`, - ); - } - return snapshots[0]; -} - -function normalizeDatasetSelection({ - version, - environment, - snapshotName, -}: DatasetSelection): DatasetSelection { - if (version !== undefined) { - return { version }; - } - - if (snapshotName !== undefined) { - return { snapshotName }; - } - - if (environment !== undefined) { - return { environment }; - } - - return {}; -} - -async function resolveDatasetSnapshotName({ - state, - datasetId, - snapshotName, -}: { - state: BraintrustState; - datasetId: string; - snapshotName: string; -}): Promise { - const match = await getDatasetSnapshot({ - state, - datasetId, - snapshotName, - }); - if (match === undefined) { - throw new Error( - `Dataset snapshot "${snapshotName}" not found for ${datasetId}`, - ); - } - return match.xact_id; -} - -async function resolveDatasetSnapshotNameForMetadata({ - state, - lazyMetadata, - snapshotName, -}: { - state: BraintrustState; - lazyMetadata: LazyValue; - snapshotName: string; -}): Promise { - const metadata = await lazyMetadata.get(); - return await resolveDatasetSnapshotName({ - state, - datasetId: metadata.dataset.id, - snapshotName, - }); -} - -async function resolveDatasetEnvironment({ - state, - datasetId, - environment, -}: { - state: BraintrustState; - datasetId: string; - environment: string; -}): Promise { - const environmentObjectPath = `environment-object/dataset/${datasetId}/${encodeURIComponent(environment)}`; - const response = - state.orgName == null - ? await state.apiConn().get_json(environmentObjectPath) - : await state.apiConn().get_json(environmentObjectPath, { - org_name: state.orgName, - }); - return z.object({ object_version: z.string() }).parse(response) - .object_version; -} - -async function resolveDatasetEnvironmentForMetadata({ - state, - lazyMetadata, - environment, -}: { - state: BraintrustState; - lazyMetadata: LazyValue; - environment: string; -}): Promise { - const metadata = await lazyMetadata.get(); - return await resolveDatasetEnvironment({ - state, - datasetId: metadata.dataset.id, - environment, - }); -} - -async function serializeDatasetForExperiment({ - dataset, - state, -}: { - dataset: AnyDataset | DatasetRef; - state: BraintrustState; -}): Promise<{ datasetId: string; datasetVersion?: string }> { - if (!Dataset.isDataset(dataset)) { - const selection = normalizeDatasetSelection(dataset); - - if (selection.version !== undefined) { - return { - datasetId: dataset.id, - datasetVersion: selection.version, - }; - } - - if (selection.snapshotName !== undefined) { - return { - datasetId: dataset.id, - datasetVersion: await resolveDatasetSnapshotName({ - state, - datasetId: dataset.id, - snapshotName: selection.snapshotName, - }), - }; - } - - if (selection.environment !== undefined) { - return { - datasetId: dataset.id, - datasetVersion: await resolveDatasetEnvironment({ - state, - datasetId: dataset.id, - environment: selection.environment, - }), - }; - } - - return { - datasetId: dataset.id, - }; - } - - const evalData = await dataset.toEvalData(); - const selection = normalizeDatasetSelection({ - version: evalData.dataset_version, - environment: evalData.dataset_environment, - snapshotName: evalData.dataset_snapshot_name, - }); - - if (selection.version !== undefined) { - return { - datasetId: evalData.dataset_id, - datasetVersion: selection.version, - }; - } - - const datasetVersion = await dataset.version(); - if (datasetVersion !== undefined) { - return { - datasetId: evalData.dataset_id, - datasetVersion, - }; - } - - return { - datasetId: evalData.dataset_id, - }; -} - -/** - * Initialize a dataset by name or ID. When initializing by name, the dataset and its project will be created if they do not exist. - * - * @param options Options for configuring initDataset(). - * @param options.project The name of the project to create the dataset in. Must specify at least one of `project` or `projectId` unless `datasetId` is provided. - * @param options.dataset The name of the dataset to create. If not specified, a name will be generated automatically. Ignored if `datasetId` is provided. - * @param options.datasetId The ID of an existing dataset. Takes precedence over `dataset` and does not require `project` or `projectId`. - * @param options.description An optional description when initializing a dataset by name. Cannot be used with `datasetId`. - * @param options.version Pin the dataset to a specific version xact_id. If `snapshotName` or `environment` are also provided, `version` takes precedence. - * @param options.snapshotName Pin the dataset to the version captured by this named snapshot. If `environment` is also provided, `snapshotName` takes precedence. - * @param options.environment Pin the dataset to the version tagged with this environment slug. - * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. - * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. - * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. - * @param options.projectId The id of the project to create the dataset in. This takes precedence over `project` if specified and is not required when `datasetId` is provided. - * @param options.metadata A dictionary with additional data when initializing a dataset by name. Cannot be used with `datasetId`. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. - * @returns The initialized Dataset. - */ -export function initDataset( - options: Readonly, -): Dataset { - const { - project, - dataset, - datasetId, - description, - version, - snapshotName, - environment, - appUrl, - apiKey, - orgName, - fetch, - forceLogin, - projectId, - metadata, - state: stateArg, - _internal_btql, - } = options; - if ( - datasetId !== undefined && - (description !== undefined || metadata !== undefined) - ) { - throw new Error( - "Cannot specify description or metadata when datasetId is provided", - ); - } - - const selection = normalizeDatasetSelection({ - version, - environment, - snapshotName, - }); - const normalizedVersion = selection.version; - const normalizedEnvironment = selection.environment; - const normalizedSnapshotName = selection.snapshotName; - - const cliInternalBtql = globalThis.__bt_eval_internal_btql; - const internalBtql = - cliInternalBtql === undefined - ? _internal_btql - : _internal_btql === undefined - ? cliInternalBtql - : isObject(_internal_btql) - ? { ...cliInternalBtql, ..._internal_btql } - : _internal_btql; - - const state = stateArg ?? _globalState; - - const lazyMetadata: LazyValue = new LazyValue( - async () => { - await state.login({ - orgName, - apiKey, - appUrl, - fetch, - forceLogin, - }); - - if (datasetId !== undefined) { - const objectInfo = datasetObjectInfoSchema.array().parse( - await state.appConn().post_json("api/self/get_object_info", { - object_type: "dataset", - object_ids: [datasetId], - }), - ); - if (objectInfo.length === 0) { - throw new Error(`Dataset with ID ${datasetId} not found`); - } - if (objectInfo.length !== 1) { - throw new Error( - `Expected exactly one dataset with ID ${datasetId}, but found ${objectInfo.length}`, - ); - } - - const datasetInfo = objectInfo[0]; - return { - project: { - id: datasetInfo.parent_cols.project.id, - name: datasetInfo.parent_cols.project.name, - fullInfo: datasetInfo.parent_cols.project, - }, - dataset: { - id: datasetInfo.object_id, - name: datasetInfo.object_name, - fullInfo: datasetInfo, - }, - }; - } - - const args: Record = { - org_id: state.orgId, - project_name: project, - project_id: projectId, - dataset_name: dataset, - description, - metadata, - }; - const response = await state - .appConn() - .post_json("api/dataset/register", args); - - return { - project: { - id: response.project.id, - name: response.project.name, - fullInfo: response.project, - }, - dataset: { - id: response.dataset.id, - name: response.dataset.name, - fullInfo: response.dataset, - }, - }; - }, - ); - - const resolvedVersion = - normalizedVersion !== undefined - ? normalizedVersion - : normalizedSnapshotName !== undefined - ? new LazyValue(async () => { - return await resolveDatasetSnapshotNameForMetadata({ - state, - lazyMetadata, - snapshotName: normalizedSnapshotName, - }); - }) - : normalizedEnvironment !== undefined - ? new LazyValue(async () => { - return await resolveDatasetEnvironmentForMetadata({ - state, - lazyMetadata, - environment: normalizedEnvironment, - }); - }) - : undefined; - - const datasetObject = new Dataset( - stateArg ?? _globalState, - lazyMetadata, - typeof resolvedVersion === "string" ? resolvedVersion : undefined, - internalBtql, - resolvedVersion instanceof LazyValue || - normalizedEnvironment !== undefined || - normalizedSnapshotName !== undefined - ? { - ...(resolvedVersion instanceof LazyValue - ? { - lazyPinnedVersion: resolvedVersion, - } - : {}), - ...(normalizedEnvironment !== undefined - ? { - pinnedEnvironment: normalizedEnvironment, - } - : {}), - ...(normalizedSnapshotName !== undefined - ? { - pinnedSnapshotName: normalizedSnapshotName, - } - : {}), - } - : undefined, - ); - - return datasetObject; -} - -// Note: the argument names *must* serialize the same way as the argument names -// for the corresponding python function, because this function may be invoked -// from arguments serialized elsewhere. -async function computeLoggerMetadata( - state: BraintrustState, - { project_name, project_id, project_group_name }: ProjectMetadataArgs, -) { - await state.login({}); - const org_id = state.orgId!; - if (!isEmpty(project_id) && !isEmpty(project_name)) { - return { - org_id, - project: { id: project_id, name: project_name, fullInfo: {} }, - }; - } - - const groupName = project_group_name || undefined; - const cache = state.projectMetadataCache; - const key = JSON.stringify([ - state.appUrl, - org_id, - state.loginToken, - isEmpty(project_id) - ? ["name", project_name || GLOBAL_PROJECT, groupName] - : ["id", project_id], - ]); - const cached = cache.get(key); - if (cached && Date.now() < cached.expiresAt) { - return structuredClone(await cached.promise); - } - - const conn = state.appConn(); - const controller = new AbortController(); - let timeout: ReturnType | undefined; - const entry = { - // The lookup timeout bounds pending requests; the cache TTL starts on success. - expiresAt: Infinity, - promise: Promise.race([ - (async (): Promise => { - if (isEmpty(project_id)) { - const response = await conn.post_json( - "api/project/register", - { - project_name: project_name || GLOBAL_PROJECT, - org_id, - project_group_name: groupName, - }, - controller.signal, - ); - return { - org_id, - project: { - id: response.project.id, - name: response.project.name, - fullInfo: response.project, - }, - }; - } - const response = await conn.get_json( - "api/project", - { id: project_id }, - 0, - controller.signal, - ); - return { - org_id, - project: { - id: project_id, - name: response.name, - fullInfo: response.project, - }, - }; - })(), - new Promise((_, reject) => { - timeout = setTimeout(() => { - const error = new Error( - "Braintrust project lookup timed out after 30 seconds.", - ); - reject(error); - controller.abort(error); - }, PROJECT_LOOKUP_TIMEOUT_MS); - }), - ]), - }; - cache.set(key, entry); - try { - const metadata = await entry.promise; - entry.expiresAt = Date.now() + PROJECT_CACHE_TTL_MS; - return structuredClone(metadata); - } catch (error) { - if (cache.get(key) === entry) { - cache.delete(key); - } - throw error; - } finally { - clearTimeout(timeout); - } -} - -export type InitLoggerOptions = FullLoginOptions & { - /** - * Capture inline media as attachments in instrumentation. Explicit booleans override - * BRAINTRUST_CAPTURE_ATTACHMENTS (1 or true); capture is otherwise disabled. - * With setCurrent: false, this logger and its child spans have an independent policy - * and an omitted option uses the environment, not the global logger's setting. - * Current loggers also update the state's default when this option is supplied; - * omitting it preserves that default. Explicitly logged attachments are unaffected. - */ - captureAttachments?: boolean; - projectName?: string; - projectId?: string; - _createInProjectGroup?: string; - environment?: SpanOriginEnvironment; - setCurrent?: boolean; - state?: BraintrustState; -}; - -/** - * Create a new logger in a specified project. If the project does not exist, it will be created. - * - * @param options Additional options for configuring init(). - * @param options.projectName The name of the project to log into. If unspecified, will default to the Global project. - * @param options.projectId The id of the project to log into. This takes precedence over projectName if specified. - * @param options._createInProjectGroup Experimental: the name of an existing project group to create the project in. Existing projects must already belong to the group, otherwise registration fails with a 409. Membership is re-checked on each fresh project lookup, so moving the project out of the group makes new loggers fail with a 409. Ignored when projectId is provided. - * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. - * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, - * if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. - * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. - * @param options.forceLogin Login again, even if you have already logged in (by default, the logger will not login if you are already logged in) - * @param options.debugLogLevel Enables internal Braintrust SDK troubleshooting output. Use `"error"`, `"warn"`, `"info"`, or `"debug"` to choose an explicit level, or `false` to explicitly disable it. If omitted, the SDK stays silent unless `BRAINTRUST_DEBUG_LOG_LEVEL` is set. - * @param setCurrent If true (the default), set the global current-experiment to the newly-created one. - * @returns The newly created Logger. - */ -export function initLogger(options: Readonly = {}): Logger { - const { - projectName, - projectId, - _createInProjectGroup, - appUrl, - apiKey, - orgName, - forceLogin, - debugLogLevel, - environment, - fetch, - state: stateArg, - } = options || {}; - - const computeMetadataArgs = { - project_name: projectName, - project_id: projectId, - project_group_name: _createInProjectGroup, - }; - - const linkArgs = { - org_name: orgName, - app_url: appUrl, - }; - - const state = stateArg ?? _globalState; - state._internalSetTraceContextSigningSecret(apiKey); - state.setDebugLogLevel(debugLogLevel); - state.spanOriginEnvironment = detectSpanOriginEnvironment(environment); - - // Enable queue size limit enforcement for initLogger() calls - // This ensures production observability doesn't OOM customer processes - state.enforceQueueSizeLimit(true); - const lazyMetadata: LazyValue = new LazyValue( - async () => { - // Otherwise actually log in. - await state.login({ - orgName, - apiKey, - appUrl, - forceLogin, - fetch, - }); - return computeLoggerMetadata(state, computeMetadataArgs); - }, - ); - - const setCurrent = options.setCurrent ?? true; - if (setCurrent && options.captureAttachments !== undefined) { - state.captureAttachments = options.captureAttachments; - } - const ret = new Logger(state, lazyMetadata, { - captureAttachments: - options.captureAttachments ?? - (setCurrent ? state.captureAttachments : undefined), - computeMetadataArgs, - linkArgs, - }); - if (setCurrent) { - state.currentLogger = ret; - } - return ret; -} - -export type LoadPromptOptions = FullLoginOptions & { - projectName?: string; - projectId?: string; - slug?: string; - version?: string; - id?: string; - defaults?: DefaultPromptArgs; - noTrace?: boolean; - environment?: string; - state?: BraintrustState; -}; - -type LoadParametersBaseOptions = FullLoginOptions & { - state?: BraintrustState; -}; - -export type LoadParametersByIdOptions = LoadParametersBaseOptions & { - id: string; - version?: string; -}; - -export type LoadParametersByIdWithEnvOptions = LoadParametersBaseOptions & { - id: string; - environment: string; - version?: string; -}; - -export type LoadParametersByProjectNameOptions = LoadParametersBaseOptions & { - projectName: string; - slug: string; - version?: string; -}; - -export type LoadParametersByProjectNameWithEnvOptions = - LoadParametersBaseOptions & { - projectName: string; - slug: string; - environment: string; - version?: string; - }; - -export type LoadParametersByProjectIdOptions = LoadParametersBaseOptions & { - projectId: string; - slug: string; - version?: string; -}; - -export type LoadParametersByProjectIdWithEnvOptions = - LoadParametersBaseOptions & { - projectId: string; - slug: string; - environment: string; - version?: string; - }; - -export type LoadParametersOptions = - | LoadParametersByIdOptions - | LoadParametersByIdWithEnvOptions - | LoadParametersByProjectNameOptions - | LoadParametersByProjectNameWithEnvOptions - | LoadParametersByProjectIdOptions - | LoadParametersByProjectIdWithEnvOptions; - -type LoadParametersImplementationOptions = LoadParametersBaseOptions & { - projectName?: string; - projectId?: string; - slug?: string; - version?: string; - id?: string; - environment?: string; -}; - -type LoaderRequestResult = - | ({ ok: true; response: T } & LoaderCacheViews) - | ({ ok: false; error: unknown } & LoaderCacheViews); - -async function runCredentialScopedLoaderRequest( - state: BraintrustState, - loginOptions: Pick< - FullLoginOptions, - "apiKey" | "appUrl" | "orgName" | "fetch" | "forceLogin" - >, - request: (requestState: LoaderRequestState) => Promise, -): Promise> { - const resolvedLoginOptions = - await state._internalResolveLoaderLoginOptions(loginOptions); - let cacheViews = state._internalGetLoaderCacheViews(resolvedLoginOptions); - - try { - const requestState = - await state._internalGetLoaderState(resolvedLoginOptions); - cacheViews = state._internalGetLoaderCacheViews( - resolvedLoginOptions, - requestState, - ); - return { - ok: true, - response: await request(requestState), - ...cacheViews, - }; - } catch (error) { - return { ok: false, error, ...cacheViews }; - } -} - -/** - * Load a prompt from the specified project. - * - * @param options Options for configuring loadPrompt(). - * @param options.projectName The name of the project to load the prompt from. Must specify at least one of `projectName` or `projectId`. - * @param options.projectId The id of the project to load the prompt from. This takes precedence over `projectName` if specified. - * @param options.slug The slug of the prompt to load. - * @param options.version An optional version of the prompt (to read). If not specified, the latest version will be used. - * @param options.environment Fetch the version of the prompt assigned to the specified environment (e.g. "production", "staging"). If both `version` and `environment` are provided, `version` takes precedence. - * @param options.id The id of a specific prompt to load. If specified, this takes precedence over all other parameters (project and slug). - * @param options.defaults (Optional) A dictionary of default values to use when rendering the prompt. Prompt values will override these defaults. - * @param options.noTrace If true, do not include logging metadata for this prompt when build() is called. - * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. - * @param options.apiKey The API key to use for this request, independently of any existing global login. If the parameter is not specified, will use an existing login or try the `BRAINTRUST_API_KEY` environment variable. In Node.js, - * if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. - * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. - * @returns The prompt object. - * @throws If the prompt is not found. - * @throws If multiple prompts are found with the same slug in the same project (this should never happen). - * - * @example - * ```javascript - * const prompt = await loadPrompt({ - * projectName: "My Project", - * slug: "my-prompt", - * }); - * ``` - */ -export async function loadPrompt({ - projectName, - projectId, - slug, - version, - environment, - id, - defaults, - noTrace = false, - appUrl, - apiKey, - orgName, - fetch, - forceLogin, - state: stateArg, -}: LoadPromptOptions) { - const versionOrEnvironment = version - ? { version } - : environment - ? { environment } - : {}; - if (id) { - // When loading by ID, we don't need project or slug - } else if (isEmpty(projectName) && isEmpty(projectId)) { - throw new Error("Must specify either projectName or projectId"); - } else if (isEmpty(slug)) { - throw new Error("Must specify slug"); - } - - const state = stateArg ?? _globalState; - const result = await runCredentialScopedLoaderRequest( - state, - { orgName, apiKey, appUrl, fetch, forceLogin }, - async (requestState) => { - let response; - if (id) { - // Load prompt by ID using the /v1/prompt/{id} endpoint - response = await requestState - .apiConn() - .get_json(`v1/prompt/${id}`, versionOrEnvironment); - // Wrap single prompt response in objects array to match list API format - if (response) { - response = { objects: [response] }; - } - } else { - response = await requestState.apiConn().get_json("v1/prompt", { - project_name: projectName, - project_id: projectId, - slug, - ...versionOrEnvironment, - }); - } - return response; - }, - ); - const { promptCache } = result; - if (!result.ok) { - const e = result.error; - if (!isLoaderCacheFallbackError(e)) { - throw e; - } - // If environment or version was specified, don't fall back to cache - if (version || environment) { - throw new Error(`Prompt not found with specified parameters: ${e}`); - } - - debugLogger - .forState(state) - .warn("Failed to load prompt, attempting to fall back to cache:", e); - let prompt; - if (id) { - prompt = await promptCache.get({ id }); - if (!prompt) { - throw new Error( - `Prompt with id ${id} not found (not found on server or in local cache): ${e}`, - ); - } - } else { - prompt = await promptCache.get({ - slug, - projectId, - projectName, - version: version ?? "latest", - }); - if (!prompt) { - throw new Error( - `Prompt ${slug} (version ${version ?? "latest"}) not found in ${[ - projectName ?? projectId, - ]} (not found on server or in local cache): ${e}`, - ); - } - } - return prompt; - } - const { response } = result; - - if (!("objects" in response) || response.objects.length === 0) { - if (id) { - throw new Error(`Prompt with id ${id} not found.`); - } else { - throw new Error( - `Prompt ${slug} not found in ${[projectName ?? projectId]}`, - ); - } - } else if (response.objects.length > 1) { - if (id) { - throw new Error( - `Multiple prompts found with id ${id}. This should never happen.`, - ); - } else { - throw new Error( - `Multiple prompts found with slug ${slug} in project ${ - projectName ?? projectId - }. This should never happen.`, - ); - } - } - - const metadata = promptSchema.parse(response["objects"][0]); - const prompt = new Prompt(metadata, defaults || {}, noTrace); - try { - if (id) { - await promptCache.set({ id }, prompt); - } else if (slug) { - await promptCache.set( - { slug, projectId, projectName, version: version ?? "latest" }, - prompt, - ); - } - } catch (e) { - debugLogger.forState(state).warn("Failed to set prompt in cache:", e); - } - return prompt; -} - -/** - * Load parameters from the specified project. - * - * @param options Options for configuring loadParameters(). - * @param options.projectName The name of the project to load the parameters from. Must specify at least one of `projectName` or `projectId`. - * @param options.projectId The id of the project to load the parameters from. This takes precedence over `projectName` if specified. - * @param options.slug The slug of the parameters to load. - * @param options.version An optional version of the parameters (to read). If not specified, the latest version will be used. - * @param options.environment Fetch the version of the parameters assigned to the specified environment (e.g. "production", "staging"). If both `version` and `environment` are provided, `version` takes precedence. - * @param options.id The id of specific parameters to load. If specified, this takes precedence over all other parameters (project and slug). - * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. - * @param options.apiKey The API key to use for this request, independently of any existing global login. If the parameter is not specified, will use an existing login or try the `BRAINTRUST_API_KEY` environment variable. In Node.js, if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. - * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. - * @returns The parameters object. - * @throws If the parameters are not found. - * @throws If multiple parameters are found with the same slug in the same project (this should never happen). - * - * @example - * ```typescript - * const params = await loadParameters({ - * projectName: "My Project", - * slug: "my-parameters", - * }); - * ``` - */ -export function loadParameters( - options: LoadParametersByProjectNameOptions, -): Promise>>; -export function loadParameters( - options: LoadParametersByProjectNameWithEnvOptions, -): Promise>>; -export function loadParameters( - options: LoadParametersByProjectIdOptions, -): Promise>>; -export function loadParameters( - options: LoadParametersByProjectIdWithEnvOptions, -): Promise>>; -export function loadParameters( - options: LoadParametersByIdOptions, -): Promise>>; -export function loadParameters( - options: LoadParametersByIdWithEnvOptions, -): Promise>>; -export async function loadParameters< - S extends EvalParameters = EvalParameters, ->({ - projectName, - projectId, - slug, - version, - environment, - id, - appUrl, - apiKey, - orgName, - fetch, - forceLogin, - state: stateArg, -}: LoadParametersImplementationOptions): Promise< - RemoteEvalParameters> -> { - const versionOrEnvironment = version - ? { version } - : environment - ? { environment } - : {}; - if (id) { - // When loading by ID, we don't need project or slug - } else if (isEmpty(projectName) && isEmpty(projectId)) { - throw new Error("Must specify either projectName or projectId"); - } else if (isEmpty(slug)) { - throw new Error("Must specify slug"); - } - - const state = stateArg ?? _globalState; - const result = await runCredentialScopedLoaderRequest( - state, - { orgName, apiKey, appUrl, fetch, forceLogin }, - async (requestState) => { - let response; - if (id) { - response = await requestState.apiConn().get_json(`v1/function/${id}`, { - ...versionOrEnvironment, - }); - if (response) { - response = { objects: [response] }; - } - } else { - response = await requestState.apiConn().get_json("v1/function", { - project_name: projectName, - project_id: projectId, - slug, - function_type: "parameters", - ...versionOrEnvironment, - }); - } - return response; - }, - ); - const { parametersCache } = result; - if (!result.ok) { - const e = result.error; - if (!isLoaderCacheFallbackError(e)) { - throw e; - } - if (version || environment) { - throw new Error(`Parameters not found with specified parameters: ${e}`); - } - - debugLogger - .forState(state) - .warn("Failed to load parameters, attempting to fall back to cache:", e); - let parameters; - if (id) { - parameters = await parametersCache.get({ id }); - if (!parameters) { - throw new Error( - `Parameters with id ${id} not found (not found on server or in local cache): ${e}`, - ); - } - } else { - parameters = await parametersCache.get({ - slug, - projectId, - projectName, - version: version ?? "latest", - }); - if (!parameters) { - throw new Error( - `Parameters ${slug} (version ${version ?? "latest"}) not found in ${[ - projectName ?? projectId, - ]} (not found on server or in local cache): ${e}`, - ); - } - } - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return parameters as RemoteEvalParameters>; - } - const { response } = result; - - if (!("objects" in response) || response.objects.length === 0) { - if (id) { - throw new Error(`Parameters with id ${id} not found.`); - } else { - throw new Error( - `Parameters ${slug} not found in ${[projectName ?? projectId]}`, - ); - } - } else if (response.objects.length > 1) { - if (id) { - throw new Error( - `Multiple parameters found with id ${id}. This should never happen.`, - ); - } else { - throw new Error( - `Multiple parameters found with slug ${slug} in project ${ - projectName ?? projectId - }. This should never happen.`, - ); - } - } - - const metadata = parametersRowSchema.parse(response["objects"][0]); - const parameters = new RemoteEvalParameters(metadata); - try { - if (id) { - await parametersCache.set({ id }, parameters); - } else if (slug) { - await parametersCache.set( - { slug, projectId, projectName, version: version ?? "latest" }, - parameters, - ); - } - } catch (e) { - debugLogger.forState(state).warn("Failed to set parameters in cache:", e); - } - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return parameters as RemoteEvalParameters>; -} - -/** - * Options for logging in to Braintrust. - */ -export interface LoginOptions { - /** - * The URL of the Braintrust App. Defaults to https://www.braintrust.dev. You should not need - * to change this unless you are doing the "Full" deployment. - */ - appUrl?: string; - /** - * The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. - */ - apiKey?: string; - /** - * The name of a specific organization to connect to. Since API keys are scoped to organizations, this parameter is usually - * unnecessary unless you are logging in with a JWT. - */ - orgName?: string; - /** - * A custom fetch implementation to use. - */ - fetch?: typeof globalThis.fetch; - /** - * By default, the SDK installs an event handler that flushes pending writes on the `beforeExit` event. - * If true, this event handler will _not_ be installed. - */ - noExitFlush?: boolean; - /** - * Calls this function if there's an error in the background flusher. - */ - onFlushError?: (error: unknown) => void; - /** - * If true, disables the local span cache used to optimize scorer access - * to trace data. When disabled, scorers will always fetch spans from the - * server. Defaults to false. - */ - disableSpanCache?: boolean; - /** - * Controls internal Braintrust SDK troubleshooting output. - * - * Use `"error"`, `"warn"`, `"info"`, or `"debug"` to control how much - * internal SDK troubleshooting output is emitted. Use `false` to explicitly - * disable this output. - * - * When omitted, the SDK remains silent unless - * `BRAINTRUST_DEBUG_LOG_LEVEL` is set to `"error"`, `"warn"`, `"info"`, or - * `"debug"`. This option only affects local console output; it does not - * change what data is logged to Braintrust. - */ - debugLogLevel?: DebugLogLevel | false; -} - -export type FullLoginOptions = LoginOptions & { - forceLogin?: boolean; -}; - -/** - * Set a global masking function that will be applied to all logged data before sending to Braintrust. - * The masking function will be applied after records are merged but before they are sent to the backend. - * Internally, masking is a state-local export customizer that runs after any - * span customizers and also covers non-span records such as dataset rows and feedback. - * - * @param maskingFunction A function that takes a JSON-serializable object and returns a masked version. - * Set to null to disable masking. - */ -export function setMaskingFunction( - maskingFunction: ((value: unknown) => unknown) | null, -): void { - _internalGetGlobalState().setMaskingFunction(maskingFunction); -} - -/** - * Log into Braintrust. This will prompt you for your API token, which you can find at - * https://www.braintrust.dev/app/token. This method is called automatically by `init()`. - * - * @param options Options for configuring login(). - * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. - * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, - * if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. - * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. - * @param options.forceLogin Login again, even if you have already logged in (by default, this function will exit quickly if you have already logged in) - */ -export async function login( - options: LoginOptions & { forceLogin?: boolean } = {}, -): Promise { - const { forceLogin = false } = options || {}; - - if (!_internalGetGlobalState()) { - _internalSetInitialState(); - } - - const state = _internalGetGlobalState(); - state.setDebugLogLevel(options.debugLogLevel); - if (state.loggedIn && !forceLogin) { - // We have already logged in. If any provided login inputs disagree with our - // existing settings, raise an Exception warning the user to try again with - // `forceLogin: true`. - function checkUpdatedParam( - varname: string, - arg: string | undefined, - orig: string | null, - ) { - if (!isEmpty(arg) && !isEmpty(orig) && arg !== orig) { - throw new Error( - `Re-logging in with different ${varname} (${arg}) than original (${orig}). To force re-login, pass \`forceLogin: true\``, - ); - } - } - checkUpdatedParam("appUrl", options.appUrl, state.appUrl); - checkUpdatedParam( - "apiKey", - options.apiKey - ? HTTPConnection.sanitize_token(options.apiKey) - : undefined, - state.loginToken, - ); - checkUpdatedParam("orgName", options.orgName, state.orgName); - return state; - } - - await state.login(options); - return state; -} - -async function loginToLoaderRequestState({ - appUrl, - apiKey, - orgName, - fetch, -}: Required> & - Pick): Promise { - let orgId: string; - let apiUrl: string; - - if (apiKey === TEST_API_KEY) { - orgId = "test-org-id"; - apiUrl = "https://braintrust.dev/fake-api-url"; - } else { - let loginResponse: Response; - try { - loginResponse = await fetch(_urljoin(appUrl, `/api/apikey/login`), { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - }); - } catch (error) { - throw new HTTPTransportError(error); - } - - const info = await readJSONResponse( - await checkResponse(loginResponse), - true, - ); - const org = selectLoginOrg(info.org_info, orgName); - orgId = org.id; - apiUrl = iso.getEnv("BRAINTRUST_API_URL") ?? org.api_url; - if (!apiUrl) { - throw new Error( - orgName - ? `Unable to log into organization '${orgName}'. Are you sure this credential is scoped to the organization?` - : "Unable to log into any organization with the provided credential.", - ); - } - } - - const apiConnection = new HTTPConnection(apiUrl, fetch, true); - apiConnection.set_token(apiKey); - apiConnection.make_long_lived(); - return { - appUrl, - orgId, - apiConn: () => apiConnection, - }; -} - -export async function loginToState(options: LoginOptions = {}) { - const { - appUrl = iso.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev", - apiKey: apiKeyArg, - orgName = iso.getEnv("BRAINTRUST_ORG_NAME"), - fetch = globalThis.fetch, - } = options || {}; - - const apiKey = - apiKeyArg !== undefined ? apiKeyArg : await iso.getBraintrustApiKey(); - const appPublicUrl = iso.getEnv("BRAINTRUST_APP_PUBLIC_URL") || appUrl; - - const state = new BraintrustState(options); - state.resetLoginInfo(); - - state.appUrl = appUrl; - state.appPublicUrl = appPublicUrl; - - let conn = null; - - if (!apiKey) { - throw new Error( - "Please specify an api key (e.g. by setting BRAINTRUST_API_KEY).", - ); - } else if (apiKey === TEST_API_KEY) { - // This is a weird hook that lets us skip logging in and mocking out the org info. - const testOrgInfo = [ - { - id: "test-org-id", - name: "test-org-name", - api_url: "https://braintrust.dev/fake-api-url", - }, - ]; - state.loggedIn = true; - state.loginToken = TEST_API_KEY; - _saveOrgInfo(state, testOrgInfo, testOrgInfo[0].name); - return state; - } else { - const controller = new AbortController(); - let timeout: ReturnType | undefined; - let info; - try { - // Bound both headers and body reads, including custom fetches that ignore - // cancellation, so a stalled request cannot block subsequent logins. - info = await Promise.race([ - (async () => { - const response = await fetch(_urljoin(appUrl, `/api/apikey/login`), { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - signal: controller.signal, - }); - return readJSONResponse(await checkResponse(response)); - })(), - new Promise((_, reject) => { - timeout = setTimeout(() => { - const error = new Error( - "Braintrust login timed out after 30 seconds.", - ); - reject(error); - controller.abort(error); - }, LOGIN_TIMEOUT_MS); - }), - ]); - } finally { - clearTimeout(timeout); - } - - _saveOrgInfo(state, info.org_info, orgName); - if (!state.apiUrl) { - if (orgName) { - throw new Error( - `Unable to log into organization '${orgName}'. Are you sure this credential is scoped to the organization?`, - ); - } else { - throw new Error( - "Unable to log into any organization with the provided credential.", - ); - } - } - - conn = state.apiConn(); - conn.set_token(apiKey); - - if (!conn) { - throw new Error("Conn should be set at this point (a bug)"); - } - - conn.make_long_lived(); - - // Set the same token in the API - state.appConn().set_token(apiKey); - if (state.proxyUrl) { - state.proxyConn().set_token(apiKey); - } - state.loginToken = conn.token; - state.loggedIn = true; - - // Replace the global logger's apiConn with this one. - state.loginReplaceApiConn(conn); - } - - return state; -} - -// XXX We should remove these global functions now -/** - * Log a single event to the current experiment. The event will be batched and uploaded behind the scenes. - * - * @param event The event to log. See {@link Experiment.log} for full details. - * @returns The `id` of the logged event. - */ -export function log(event: ExperimentLogFullArgs): string { - debugLogger.warn( - "braintrust.log is deprecated and will be removed in a future version of braintrust. Use `experiment.log` instead.", - ); - const e = currentExperiment(); - if (!e) { - throw new Error("Not initialized. Please call init() first"); - } - return e.log(event); -} - -/** - * Summarize the current experiment, including the scores (compared to the closest reference experiment) and metadata. - * - * @param options Options for summarizing the experiment. - * @param options.summarizeScores Whether to summarize the scores. If False, only the metadata will be returned. - * @param options.comparisonExperimentId The experiment to compare against. If None, the most recent experiment on the origin's main branch will be used. - * @returns A summary of the experiment, including the scores (compared to the closest reference experiment) and metadata. - */ -export async function summarize( - options: { - readonly summarizeScores?: boolean; - readonly comparisonExperimentId?: string; - } = {}, -): Promise { - debugLogger.warn( - "braintrust.summarize is deprecated and will be removed in a future version of braintrust. Use `experiment.summarize` instead.", - ); - const e = currentExperiment(); - if (!e) { - throw new Error("Not initialized. Please call init() first"); - } - return await e.summarize(options); -} - -type OptionalStateArg = { - state?: BraintrustState; -}; - -/** - * Returns the currently-active experiment (set by {@link init}). Returns undefined if no current experiment has been set. - */ -export function currentExperiment( - options?: OptionalStateArg, -): Experiment | undefined { - const state = options?.state ?? _globalState; - return state.currentExperiment; -} - -/** - * Returns the currently-active logger (set by {@link initLogger}). Returns undefined if no current logger has been set. - */ -export function currentLogger(options?: OptionalStateArg): Logger | undefined { - const state = options?.state ?? _globalState; - return state.currentLogger; -} - -/** - * Return the currently-active span for logging (set by one of the `traced` methods). If there is no active span, returns a no-op span object, which supports the same interface as spans but does no logging. - * - * See {@link Span} for full details. - */ -export function currentSpan(options?: OptionalStateArg): Span { - const state = options?.state ?? _globalState; - return state.contextManager.getCurrentSpan() ?? NOOP_SPAN; -} - -/** - * Resolve the parent object and any forwarded W3C state in a single pass. - * - * Same precedence as {@link getSpanParentObject}, but also returns the W3C state - * (tracestate + raw traceparent flags) recovered while normalizing the `parent` - * argument, so callers don't have to re-run `normalizeParent` (which would - * re-parse baggage and re-resolve the active logger/experiment, potentially - * disagreeing if state changed between calls). The state is only meaningful when - * a parent slug was resolved; otherwise it is undefined. - */ -function getSpanParentObjectAndPropagatedState( - options?: OptionalStateArg & { parent?: string | PropagationContext }, -): { - parentObject: - | SpanComponentsV3 - | SpanComponentsV4 - | Span - | Experiment - | Logger; - propagatedState: PropagatedState | undefined; -} { - const state = options?.state ?? _globalState; - - const parentSpan = currentSpan({ state }); - if (!Object.is(parentSpan, NOOP_SPAN)) { - return { parentObject: parentSpan, propagatedState: undefined }; - } - - const parent = options?.parent ?? state.currentParent.getStore(); - const { parentSlug, propagatedState } = normalizeParent(parent, state); - if (parentSlug) { - return { - parentObject: SpanComponentsV4.fromStr(parentSlug), - propagatedState, - }; - } - - const experiment = state.currentExperiment; - if (experiment) { - return { parentObject: experiment, propagatedState: undefined }; - } - const logger = state.currentLogger; - if (logger) { - return { parentObject: logger, propagatedState: undefined }; - } - return { parentObject: NOOP_SPAN, propagatedState: undefined }; -} - -/** - * Mainly for internal use. Return the parent object for starting a span in a global context. - * Applies precedence: current span > propagated parent > experiment > logger. - * - * `parent` may be an exported slug string or an opaque W3C trace-context (from - * {@link extractTraceContextFromHeaders}). - */ -export function getSpanParentObject( - options?: OptionalStateArg & { parent?: string | PropagationContext }, -): SpanComponentsV3 | SpanComponentsV4 | Span | Experiment | Logger { - return getSpanParentObjectAndPropagatedState(options).parentObject; -} - -/** @internal */ -export function _internalExportParentSynchronously( - parent: ReturnType, -): string | undefined { - if ("toStr" in parent) { - return parent.toStr(); - } - - if ("getParentInfo" in parent) { - const parentInfo = parent.getParentInfo(); - if (!parentInfo) { - return undefined; - } - const objectId = parentInfo.objectId.getSync().value; - if (!objectId && !parentInfo.computeObjectMetadataArgs) { - return undefined; - } - return new SpanComponentsV4({ - object_type: parentInfo.objectType, - ...(objectId - ? { object_id: objectId } - : { - compute_object_metadata_args: - parentInfo.computeObjectMetadataArgs ?? {}, - }), - row_id: parent.id, - root_span_id: parent.rootSpanId, - span_id: parent.spanId, - }).toStr(); - } - - const components = braintrustParentToComponents(parent._getOtelParent()); - if (!components) { - return undefined; - } - return new SpanComponentsV4({ - object_type: components.objectType, - ...(components.objectId - ? { object_id: components.objectId } - : { compute_object_metadata_args: components.computeArgs ?? {} }), - }).toStr(); -} - -/** - * Return the Braintrust parent string for the current logger/experiment, if any. - * - * Used as the fallback Braintrust parent on receive, when an inbound request - * carries trace identity (`traceparent`) but no `braintrust.parent` baggage. - */ -function currentBraintrustParent(state?: BraintrustState): string | undefined { - const resolvedState = state ?? _globalState; - - const experiment = currentExperiment({ state: resolvedState }); - if (experiment) { - try { - return experiment._getOtelParent() ?? undefined; - } catch { - return undefined; - } - } - - const logger = currentLogger({ state: resolvedState }); - if (logger) { - try { - return logger._getOtelParent() ?? undefined; - } catch { - return undefined; - } - } - - return undefined; -} - -interface BraintrustParentComponents { - objectType: SpanObjectTypeV3; - objectId: string | undefined; - computeArgs: Record | undefined; -} - -/** - * Parse a `braintrust.parent` string into object type / id / compute args. - * - * Accepts `project_id:`, `project_name:`, or `experiment_id:`. - * Returns undefined if the value is empty or malformed. - */ -function braintrustParentToComponents( - braintrustParent: string | undefined | null, -): BraintrustParentComponents | undefined { - if (!braintrustParent) { - return undefined; - } - if (braintrustParent.startsWith("project_id:")) { - const objectId = braintrustParent.slice("project_id:".length); - return objectId - ? { - objectType: SpanObjectTypeV3.PROJECT_LOGS, - objectId, - computeArgs: undefined, - } - : undefined; - } - if (braintrustParent.startsWith("project_name:")) { - const name = braintrustParent.slice("project_name:".length); - return name - ? { - objectType: SpanObjectTypeV3.PROJECT_LOGS, - objectId: undefined, - computeArgs: { project_name: name }, - } - : undefined; - } - if (braintrustParent.startsWith("experiment_id:")) { - const objectId = braintrustParent.slice("experiment_id:".length); - return objectId - ? { - objectType: SpanObjectTypeV3.EXPERIMENT, - objectId, - computeArgs: undefined, - } - : undefined; - } - return undefined; -} - -/** - * Set a W3C trace-context header on a carrier, sending the lowercase name. - * - * Per the W3C Trace Context spec (§3.2.1 / §3.3.1), vendors SHOULD send these - * header names in lowercase. `name` is always the canonical lowercase key. A - * plain object carrier is case-sensitive, so any pre-existing case-variant (e.g. - * `Baggage` from a framework that title-cases headers) must be removed first, - * otherwise the carrier would end up with two conflicting headers. - */ -function isMutableHeaderTupleArray( - carrier: TraceContextCarrier, -): carrier is [string, string][] { - return ( - Array.isArray(carrier) && - carrier.every((item) => Array.isArray(item) && typeof item[0] === "string") - ); -} - -function setHeader( - carrier: TraceContextCarrier, - name: string, - value: string, -): void { - if (isMutableHeaderTupleArray(carrier)) { - const lowered = name.toLowerCase(); - for (let i = carrier.length - 1; i >= 0; i--) { - if (carrier[i][0].toLowerCase() === lowered) { - carrier.splice(i, 1); - } - } - carrier.push([name, value]); - return; - } - - const setter = (carrier as { set?: unknown }).set; - if (typeof setter === "function") { - const deleter = (carrier as { delete?: unknown }).delete; - if (typeof deleter === "function") { - try { - deleter.call(carrier, name); - } catch { - // Setting below may still succeed for custom header-like objects. - } - } - setter.call(carrier, name, value); - return; - } - - const nodeSetter = (carrier as { setHeader?: unknown }).setHeader; - if (typeof nodeSetter === "function") { - const remover = (carrier as { removeHeader?: unknown }).removeHeader; - if (typeof remover === "function") { - try { - remover.call(carrier, name); - } catch { - // Setting below may still succeed for custom header-like objects. - } - } - nodeSetter.call(carrier, name, value); - return; - } - - const headerSetter = (carrier as { header?: unknown }).header; - if (typeof headerSetter === "function") { - const deleter = (carrier as { delete?: unknown }).delete; - if (typeof deleter === "function") { - try { - deleter.call(carrier, name); - } catch { - // Setting below may still succeed for custom header-like objects. - } - } - const remover = (carrier as { removeHeader?: unknown }).removeHeader; - if (typeof remover === "function") { - try { - remover.call(carrier, name); - } catch { - // Setting below may still succeed for custom header-like objects. - } - } - headerSetter.call(carrier, name, value); - return; - } - - const headerBag = carrier as { [name: string]: string }; - const lowered = name.toLowerCase(); - for (const key of Object.keys(headerBag)) { - if (key !== name && key.toLowerCase() === lowered) { - delete headerBag[key]; - } - } - headerBag[name] = value; -} - -function deleteHeader(carrier: TraceContextCarrier, name: string): void { - const lowered = name.toLowerCase(); - if (isMutableHeaderTupleArray(carrier)) { - for (let i = carrier.length - 1; i >= 0; i--) { - if (carrier[i][0].toLowerCase() === lowered) { - carrier.splice(i, 1); - } - } - return; - } - - const deleter = (carrier as { delete?: unknown }).delete; - if (typeof deleter === "function") { - try { - deleter.call(carrier, name); - return; - } catch { - // Fall through to other header carrier styles. - } - } - - const remover = (carrier as { removeHeader?: unknown }).removeHeader; - if (typeof remover === "function") { - try { - remover.call(carrier, name); - return; - } catch { - // Fall through to plain object cleanup. - } - } - - const headerBag = carrier as { [name: string]: string }; - for (const key of Object.keys(headerBag)) { - if (key.toLowerCase() === lowered) { - delete headerBag[key]; - } - } -} - -/** - * Inject W3C trace-context headers into a carrier (in place). - * - * Emits `traceparent` from the hex trace/span ids, merges `braintrust.parent` - * into existing `baggage` when known, and forwards any inbound W3C state - * (`tracestate` plus the original `traceparent` trace-flags) carried in - * `propagatedState`. Pre-existing, non-Braintrust baggage entries are preserved. - */ -export function _injectIntoCarrier( - carrier: TraceContextCarrier, - args: { - traceId: string | undefined; - spanId: string | undefined; - braintrustParent: string | undefined | null; - propagatedState?: PropagatedState | undefined; - }, -): void { - // Re-emit the inbound trace-flags verbatim so the upstream sampling decision - // (and any future flag bits) is preserved; defaults to sampled when we - // originated the trace (no inbound flags). - const traceFlags = args.propagatedState?.traceFlags; - const traceparent = traceFlags - ? formatTraceparent(args.traceId, args.spanId, traceFlags) - : formatTraceparent(args.traceId, args.spanId); - if (traceparent === undefined) { - // Ids aren't W3C-shaped (e.g. legacy UUID mode); nothing to propagate. - return; - } - setHeader(carrier, TRACEPARENT_HEADER, traceparent); - - // Forward upstream tracestate (per W3C, only alongside a valid traceparent). - const tracestate = args.propagatedState?.tracestate; - if (tracestate) { - setHeader(carrier, TRACESTATE_HEADER, tracestate); - } - - // Merge braintrust.parent into any existing baggage. Other vendors' members - // are forwarded byte-for-byte (see mergeBaggage) so we never rewrite their - // percent-encoding. - const existing = getHeader(carrier, BAGGAGE_HEADER); - const baggageValue = mergeBaggage(existing, args.braintrustParent); - if (baggageValue !== undefined) { - setHeader(carrier, BAGGAGE_HEADER, baggageValue); - } else if (existing !== undefined) { - // mergeBaggage returns undefined when the existing header only had a stale - // braintrust.parent (or otherwise no members worth forwarding), so remove - // the old header instead of letting a reused carrier propagate bad routing. - // If left in place, a downstream service could attach this trace to the - // previous project/experiment, or leak that parent to an unrelated request. - deleteHeader(carrier, BAGGAGE_HEADER); - } -} - -/** - * Inject W3C trace-context headers for the current (or given) span into a - * carrier. - * - * This is the free-function form of {@link Span.inject}, and the send-side - * counterpart of {@link extractTraceContextFromHeaders}. If no span is - * provided, the currently-active span is used. Propagation is best-effort and - * never throws. - * - * @param carrier Optional carrier (e.g. outbound HTTP headers) to mutate. - * Supports plain objects, Web `Headers`, Node-style `setHeader` carriers, - * framework `header` carriers, and mutable `HeadersInit` tuple arrays. - * @param options.span Optional span to inject. Defaults to the current span. - * @returns The carrier with propagation headers injected. - */ -export function injectTraceContext( - carrier?: undefined, - options?: OptionalStateArg & { span?: Span }, -): Record; -export function injectTraceContext( - carrier: T, - options?: OptionalStateArg & { span?: Span }, -): T; -export function injectTraceContext( - carrier?: T, - options?: OptionalStateArg & { span?: Span }, -): T | Record { - const resolvedCarrier = carrier ?? {}; - const span = options?.span ?? currentSpan({ state: options?.state }); - try { - return span.inject(resolvedCarrier); - } catch (e) { - debugLogger.warn(`Error injecting trace context: ${e}`); - return resolvedCarrier; - } -} - -/** - * Extract an opaque W3C trace-context from inbound request headers. - * - * This is the receive-side counterpart of {@link Span.inject} / - * {@link injectTraceContext}. The return value is an opaque propagation context - * that can be passed as `parent` to `startSpan`: - * - * ```ts - * const ctx = extractTraceContextFromHeaders(request.headers); - * traced((span) => { ... }, { name: "handler", parent: ctx }); - * ``` - * - * Only the W3C Trace Context headers are interpreted (`traceparent`, `baggage`, - * `tracestate`); header lookups are case-insensitive. If no valid `traceparent` - * is present, returns undefined (the caller starts a fresh root span). The - * Braintrust container the trace is routed under is resolved when the span is - * created: from the `braintrust.parent` baggage entry, or else the - * currently-active logger/experiment. - * - * Callers should treat the return value as opaque. - * - * @param headers Inbound request headers (e.g. an HTTP framework's headers). - * @returns An opaque context for `startSpan({ parent })`, or undefined. - */ -export function extractTraceContextFromHeaders( - headers: TraceContextHeaders | null | undefined, -): PropagationContext | undefined { - if (!headers) { - return undefined; - } - - const traceparent = getHeader(headers, TRACEPARENT_HEADER); - if (!traceparent || parseTraceparent(traceparent) === undefined) { - return undefined; - } - - const context: PropagationContext = { [TRACEPARENT_HEADER]: traceparent }; - const baggageValue = getHeader(headers, BAGGAGE_HEADER); - if (baggageValue) { - context[BAGGAGE_HEADER] = baggageValue; - } - const tracestate = getHeader(headers, TRACESTATE_HEADER); - if (tracestate) { - // `tracestate` is opaque vendor state that we forward but never author or - // interpret. It is passed along unchanged; receivers that want to use it - // validate it themselves, per the W3C Trace Context spec. - context[TRACESTATE_HEADER] = tracestate; - } - return context; -} - -/** - * Resolve a W3C trace-context into `{ parentSlug, propagatedState }`. - * - * Reads `traceparent` for trace identity and `braintrust.parent` from `baggage` - * (falling back to the currently-active logger/experiment) for routing, and - * builds an internal Braintrust parent slug. Captures the `tracestate` and raw - * `traceparent` flags to forward onward. Returns `{ undefined, undefined }` if - * the context cannot be resolved into a usable parent (so the caller falls back - * to local precedence / a fresh root). - */ -function resolveW3cParent( - context: PropagationContext, - state?: BraintrustState, -): { - parentSlug: string | undefined; - propagatedState: PropagatedState | undefined; -} { - const traceparent = getHeader(context, TRACEPARENT_HEADER); - const parsed = traceparent ? parseTraceparent(traceparent) : undefined; - if (parsed === undefined) { - return { parentSlug: undefined, propagatedState: undefined }; - } - const { traceId, spanId, traceFlags } = parsed; - - // Determine the Braintrust container: baggage -> current logger/experiment. - let braintrustParent: string | undefined = undefined; - const baggageValue = getHeader(context, BAGGAGE_HEADER); - if (baggageValue) { - braintrustParent = parseBaggage(baggageValue)[BRAINTRUST_PARENT_KEY]; - } - if (!braintrustParent) { - braintrustParent = currentBraintrustParent(state); - } - if (!braintrustParent) { - debugLogger.warn( - "Received traceparent without a braintrust.parent and no active logger/experiment; " + - "cannot route the trace. Starting a fresh local span instead.", - ); - return { parentSlug: undefined, propagatedState: undefined }; - } - - const parsedParent = braintrustParentToComponents(braintrustParent); - if (parsedParent === undefined) { - debugLogger.warn( - `Invalid braintrust.parent: ${JSON.stringify(braintrustParent)}`, - ); - return { parentSlug: undefined, propagatedState: undefined }; - } - const { objectType, objectId, computeArgs } = parsedParent; - - // `tracestate` is opaque vendor state, forwarded unchanged and never - // authored or interpreted by Braintrust. - const tracestate = getHeader(context, TRACESTATE_HEADER); - - const slug = new SpanComponentsV4({ - object_type: objectType, - ...(computeArgs - ? { compute_object_metadata_args: computeArgs } - : { object_id: objectId }), - row_id: "bt-propagation", // non-empty to enable span_id/root_span_id - span_id: spanId, - root_span_id: traceId, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - } as SpanComponentsV4Data).toStr(); - return { - parentSlug: slug, - propagatedState: { tracestate, traceFlags, braintrustParent }, - }; -} - -/** - * Normalize a `parent` argument into `{ parentSlug, propagatedState }`. - * - * - object -> interpreted as a W3C trace-context (from - * `extractTraceContextFromHeaders`) - * - string -> an exported span slug (passed through unchanged) - * - undefined -> no parent - * - * `propagatedState` carries any inbound W3C state (tracestate + raw flags) to - * forward on the next `inject`; it is undefined when there is no inbound W3C - * context. - */ -function normalizeParent( - parent: string | PropagationContext | undefined | null, - state?: BraintrustState, -): { - parentSlug: string | undefined; - propagatedState: PropagatedState | undefined; -} { - if (parent && typeof parent === "object") { - return resolveW3cParent(parent, state); - } - return { - parentSlug: parent ?? undefined, - propagatedState: undefined, - }; -} - -/** - * Return true if a parent slug carries span ids the child can link to. - * - * A child links to any parent slug that carries both a span id and a root span - * id, regardless of whether those ids are hex (the default) or legacy UUID. This - * keeps the deprecated `startSpan({ parent: })` path backwards - * compatible: a slug exported by an older (UUID) sender still links into a trace - * on a newer (hex) receiver, and vice versa. The child's own freshly generated - * span id stays in the active format; the backend supports traces whose nodes - * mix id formats across a propagation boundary. Empty ids (no row) are handled - * by the caller. - */ -function parentSpanIdsUsable( - spanId: string | undefined | null, - rootSpanId: string | undefined | null, -): boolean { - return Boolean(spanId) && Boolean(rootSpanId); -} - -export function logError(span: Span, error: unknown) { - let errorMessage = ""; - let stackTrace = ""; - if (error instanceof Error) { - errorMessage = error.message; - stackTrace = error.stack || ""; - } else { - errorMessage = String(error); - } - span.log({ error: `${errorMessage}\n\n${stackTrace}` }); -} - -/** - * Toplevel function for starting a span. It checks the following (in precedence order): - * * Currently-active span - * * Currently-active experiment - * * Currently-active logger - * - * and creates a span under the first one that is active. Alternatively, if `parent` is specified, it creates a span under the specified parent row. If none of these are active, it returns a no-op span object. - * - * See {@link Span.traced} for full details. - */ -export function traced( - callback: (span: Span) => R, - args?: StartSpanArgs & SetCurrentArg & OptionalStateArg, -): R { - const { setCurrent, ...spanArgs } = args ?? {}; - const span = startSpanImpl(spanArgs); - - return runCatchFinally( - () => { - if (setCurrent ?? true) { - return withCurrent(span, callback); - } else { - return callback(span); - } - }, - (error) => { - logError(span, error); - throw error; - }, - () => span.end(), - ); -} - -type WrapTracedArgs = { - noTraceIO?: boolean; -}; - -/** - * Check if a function is a sync generator function. - * - * Note: This uses Object.prototype.toString which is sufficient for environments that - * support generator functions (ES6+). While our code is compiled to ES2022, consumers - * may run it in various environments. However, if generators aren't supported in their - * environment, the generator functions themselves won't work anyway, making detection moot. - * - * @param fn The function to check. - * @returns True if the function is a sync generator function. - */ -function isGeneratorFunction(fn: unknown): boolean { - return Object.prototype.toString.call(fn) === "[object GeneratorFunction]"; -} - -/** - * Check if a function is an async generator function. - * - * Note: see isGeneratorFunction disclaimer - * @param fn The function to check. - * @returns True if the function is an async generator function. - */ -function isAsyncGeneratorFunction(fn: unknown): boolean { - return ( - Object.prototype.toString.call(fn) === "[object AsyncGeneratorFunction]" - ); -} - -/** - * Wrap a sync generator function with tracing. - */ -function wrapTracedSyncGenerator any>( - fn: F, - spanArgs: StartSpanArgs & SetCurrentArg, - noTraceIO: boolean, -): F { - const wrapper = function* ( - this: ThisParameterType, - ...fnArgs: Parameters - ) { - const span = startSpan(spanArgs); - try { - if (!noTraceIO) { - span.log({ input: fnArgs }); - } - - const maxItems = getNumericEnv("BRAINTRUST_MAX_GENERATOR_ITEMS") ?? 1000; - - if (!noTraceIO && maxItems !== 0) { - let collected: unknown[] = []; - let truncated = false; - - const gen = generatorWithCurrent(span, fn.apply(this, fnArgs)); - try { - for (const value of gen) { - if ( - maxItems === -1 || - (!truncated && collected.length < maxItems) - ) { - collected.push(value); - } else { - truncated = true; - collected = []; - debugLogger.warn( - `Generator output exceeded limit of ${maxItems} items, output not logged. ` + - `Increase BRAINTRUST_MAX_GENERATOR_ITEMS or set to -1 to disable limit.`, - ); - } - yield value; - } - - if (!truncated) { - span.log({ output: collected }); - } - } catch (error) { - logError(span, error); - if (!truncated && collected.length > 0) { - span.log({ output: collected }); - } - throw error; - } - } else { - // No output collection - const gen = generatorWithCurrent(span, fn.apply(this, fnArgs)); - for (const value of gen) { - yield value; - } - } - } finally { - span.end(); - } - }; - // Preserve the original function name - Object.defineProperty(wrapper, "name", { value: fn.name }); - return wrapper as F; -} - -/** - * Wrap an async generator function with tracing. - */ -function wrapTracedAsyncGenerator any>( - fn: F, - spanArgs: StartSpanArgs & SetCurrentArg, - noTraceIO: boolean, -): F { - const wrapper = async function* ( - this: ThisParameterType, - ...fnArgs: Parameters - ) { - const span = startSpan(spanArgs); - try { - if (!noTraceIO) { - span.log({ input: fnArgs }); - } - - const maxItems = getNumericEnv("BRAINTRUST_MAX_GENERATOR_ITEMS") ?? 1000; - - if (!noTraceIO && maxItems !== 0) { - let collected: unknown[] = []; - let truncated = false; - - const gen = asyncGeneratorWithCurrent(span, fn.apply(this, fnArgs)); - try { - for await (const value of gen) { - if ( - maxItems === -1 || - (!truncated && collected.length < maxItems) - ) { - collected.push(value); - } else { - truncated = true; - collected = []; - debugLogger.warn( - `Generator output exceeded limit of ${maxItems} items, output not logged. ` + - `Increase BRAINTRUST_MAX_GENERATOR_ITEMS or set to -1 to disable limit.`, - ); - } - yield value; - } - - if (!truncated) { - span.log({ output: collected }); - } - } catch (error) { - logError(span, error); - if (!truncated && collected.length > 0) { - span.log({ output: collected }); - } - throw error; - } - } else { - // No output collection - const gen = asyncGeneratorWithCurrent(span, fn.apply(this, fnArgs)); - for await (const value of gen) { - yield value; - } - } - } finally { - span.end(); - } - }; - // Preserve the original function name - Object.defineProperty(wrapper, "name", { value: fn.name }); - return wrapper as F; -} - -/** - * Wrap a function with `traced`, using the arguments as `input` and return value as `output`. - * Any functions wrapped this way will automatically be traced, similar to the `@traced` decorator - * in Python. If you want to correctly propagate the function's name and define it in one go, then - * you can do so like this: - * - * ```ts - * const myFunc = wrapTraced(async function myFunc(input) { - * const result = await client.chat.completions.create({ - * model: "gpt-3.5-turbo", - * messages: [{ role: "user", content: input }], - * }); - * return result.choices[0].message.content ?? "unknown"; - * }, - * // Optional: if you're using a framework like NextJS that minifies your code, specify the function name and it will be used for the span name - * { name: "myFunc" }, - * ); - * ``` - * Now, any calls to `myFunc` will be traced, and the input and output will be logged automatically. - * If tracing is inactive, i.e. there is no active logger or experiment, it's just a no-op. - * - * @param fn The function to wrap. - * @param args Span-level arguments (e.g. a custom name or type) to pass to `traced`. - * @returns The wrapped function. - */ -export function wrapTraced any>( - fn: F, - args?: StartSpanArgs & SetCurrentArg & WrapTracedArgs, -): F { - const { noTraceIO, ...argsRest } = args ?? {}; - const spanArgs: StartSpanArgs & SetCurrentArg = { - name: fn.name, - type: "function", - ...argsRest, - }; - const hasExplicitInput = spanArgs.event?.input !== undefined; - const hasExplicitOutput = spanArgs.event?.output !== undefined; - - const disableGeneratorTraceIO = - !!noTraceIO || hasExplicitInput || hasExplicitOutput; - // Check if the function is a generator - if (isGeneratorFunction(fn)) { - return wrapTracedSyncGenerator(fn, spanArgs, disableGeneratorTraceIO); - } - - if (isAsyncGeneratorFunction(fn)) { - return wrapTracedAsyncGenerator(fn, spanArgs, disableGeneratorTraceIO); - } - - return ((...fnArgs: Parameters) => - traced((span) => { - if (!noTraceIO && !hasExplicitInput) { - span.log({ input: fnArgs }); - } - - const output = fn(...fnArgs); - - if (!noTraceIO && !hasExplicitOutput) { - if (output instanceof Promise) { - return (async () => { - const result = await output; - span.log({ output: result }); - return result; - })(); - } else { - span.log({ output }); - } - } - - return output; - }, spanArgs)) as F; -} - -/** - * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations - * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", - * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead. - * - * See {@link traced} for full details. - */ -export function startSpan(args?: StartSpanArgs & OptionalStateArg): Span { - return startSpanImpl(args); -} - -export function _internalStartSpan( - args?: InternalStartSpanArgs & OptionalStateArg, -): Span { - return startSpanImpl(args); -} - -/** @internal Start a span whose initial row is merged with concurrent writes. */ -export function _internalStartSpanWithInitialMerge( - args?: InternalStartSpanArgs & OptionalStateArg, -): Span { - return startSpanImpl({ - ...args, - [INITIAL_SPAN_WRITE_AS_MERGE]: true, - } as InternalStartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg); -} - -/** @internal Start a deterministic span under an exported object-only parent. */ -export function _internalStartSpanWithInitialMergeAndParentSpanIds( - args: InternalStartSpanArgs & OptionalStateArg, -): Span { - return startSpanImpl( - { - ...args, - [INITIAL_SPAN_WRITE_AS_MERGE]: true, - } as InternalStartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg, - { useParentSpanIdsForObjectParent: true }, - ); -} - -/** @internal Start a span with SDK-controlled context fields. */ -export function _internalStartSpanWithContext( - args: InternalStartSpanArgs & OptionalStateArg, - context: Record, -): Span { - return startSpanImpl({ - ...args, - [INTERNAL_SPAN_CONTEXT]: context, - }); -} - -/** - * Flush any pending rows to the server. - */ -export async function flush(options?: OptionalStateArg): Promise { - const state = options?.state ?? _globalState; - return await state.bgLogger().flush(); -} - -/** - * Set the fetch implementation to use for requests. You can specify it here, - * or when you call `login`. - * - * @param fetch The fetch implementation to use. - */ -export function setFetch(fetch: typeof globalThis.fetch): void { - _internalGetGlobalState().setFetch(fetch); -} - -function startSpanImpl( - args?: InternalStartSpanArgs & OptionalStateArg & InternalSpanContextArg, - internalOptions?: { useParentSpanIdsForObjectParent?: boolean }, -): Span { - const state = args?.state ?? _globalState; - - // Resolve the parent object and any forwarded W3C state in one pass, so we - // don't re-normalize `parent` (which could disagree if the active - // logger/experiment changed between calls). - const { parentObject, propagatedState } = - getSpanParentObjectAndPropagatedState(args); - - if ( - parentObject instanceof SpanComponentsV3 || - parentObject instanceof SpanComponentsV4 - ) { - const parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined = - parentObject.data.row_id && - parentSpanIdsUsable( - parentObject.data.span_id, - parentObject.data.root_span_id, - ) - ? { - spanId: parentObject.data.span_id, - rootSpanId: parentObject.data.root_span_id, - } - : internalOptions?.useParentSpanIdsForObjectParent - ? args?.parentSpanIds - : undefined; - // The parent object/state are already resolved from `parent` above; drop - // the raw `parent` so it isn't re-normalized. - const { parent: _ignoredParent, ...spanArgs } = args ?? {}; - return new SpanImpl({ - state, - ...spanArgs, - parentObjectType: parentObject.data.object_type, - parentObjectId: new LazyValue( - spanComponentsToObjectIdLambda(state, parentObject), - ), - parentComputeObjectMetadataArgs: - parentObject.data.compute_object_metadata_args ?? undefined, - parentSpanIds, - propagatedEvent: - args?.propagatedEvent ?? - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ((parentObject.data.propagated_event ?? undefined) as - | StartSpanEventArgs - | undefined), - propagatedState, - }); - } else { - // The internal entrypoint still accepts exported Braintrust parent slugs. - // Span implementations support that runtime form even though it is no - // longer part of their public method signature. - return parentObject.startSpan(args as StartSpanArgs); - } -} - -/** - * Runs the provided callback with the span as the current span. - */ -export function withCurrent( - span: Span, - callback: (span: Span) => R, - state: BraintrustState | undefined = undefined, -): R { - const currentState = state ?? _globalState; - return currentState.contextManager.runInContext(span, () => callback(span)); -} - -/** - * Wraps a sync generator to maintain the current span context across yields. - */ -function* generatorWithCurrent( - span: Span, - gen: Generator, - state: BraintrustState | undefined = undefined, -): Generator { - let nextValue: any; - while (true) { - const result = withCurrent( - span, - () => { - try { - return gen.next(nextValue); - } catch (e) { - // Handle generator.throw() - return { value: undefined, done: true, error: e }; - } - }, - state, - ); - - if ("error" in result) { - throw result.error; - } - - if (result.done) { - return result.value; - } - - nextValue = yield result.value; - } -} - -/** - * Wraps an async generator to maintain the current span context across yields. - */ -async function* asyncGeneratorWithCurrent( - span: Span, - gen: AsyncGenerator, - state: BraintrustState | undefined = undefined, -): AsyncGenerator { - let nextValue: any; - while (true) { - const result = await withCurrent( - span, - async () => { - try { - return await gen.next(nextValue); - } catch (e) { - // Handle generator.throw() - return { value: undefined, done: true, error: e }; - } - }, - state, - ); - - if ("error" in result) { - throw result.error; - } - - if (result.done) { - return result.value; - } - - nextValue = yield result.value; - } -} - -export function withParent( - parent: PropagationContext, - callback: () => R, - state: BraintrustState | undefined = undefined, -): R { - return (state ?? _globalState).currentParent.run(parent, () => callback()); -} - -export function _internalWithParent( - parent: string | PropagationContext, - callback: () => R, - state: BraintrustState | undefined = undefined, -): R { - return (state ?? _globalState).currentParent.run(parent, () => callback()); -} - -function _saveOrgInfo( - state: BraintrustState, - orgInfo: any, - orgName: string | undefined, -) { - const org = selectLoginOrg(orgInfo, orgName); - state.orgId = org.id; - state.orgName = org.name; - state.apiUrl = iso.getEnv("BRAINTRUST_API_URL") ?? org.api_url; - state.proxyUrl = iso.getEnv("BRAINTRUST_PROXY_URL") ?? org.proxy_url; - state.gitMetadataSettings = org.git_metadata || undefined; -} - -function selectLoginOrg(orgInfo: any, orgName: string | undefined) { - if (orgInfo.length === 0) { - throw new LoginInvalidOrgError( - "This user is not part of any organizations.", - ); - } - - for (const org of orgInfo) { - if (orgName === undefined || org.name === orgName) { - return org; - } - } - - throw new LoginInvalidOrgError( - `Organization ${orgName} not found. Must be one of ${orgInfo - .map((org: any) => org.name) - .join(", ")}`, - ); -} - -function validateTags(tags: readonly string[]) { - const seen = new Set(); - for (const tag of tags) { - if (typeof tag !== "string") { - throw new Error("tags must be strings"); - } - - if (seen.has(tag)) { - throw new Error(`duplicate tag: ${tag}`); - } - seen.add(tag); - } -} - -function validateAndSanitizeExperimentLogPartialArgs( - event: ExperimentLogPartialArgs, -): SanitizedExperimentLogPartialArgs { - if (event.scores) { - if (Array.isArray(event.scores)) { - throw new Error("scores must be an object, not an array"); - } - for (const [name, rawScore] of Object.entries(event.scores)) { - let score = rawScore; - if (typeof name !== "string") { - throw new Error("score names must be strings"); - } - - if (score === null || score === undefined) { - continue; - } - - if (typeof score === "boolean") { - score = score ? 1 : 0; - event.scores[name] = score; - } - - if (typeof score !== "number") { - throw new Error("score values must be numbers"); - } - if (score < 0 || score > 1) { - throw new Error("score values must be between 0 and 1"); - } - } - } - - if (event.metadata) { - for (const key of Object.keys(event.metadata)) { - if (typeof key !== "string") { - throw new Error("metadata keys must be strings"); - } - } - } - - if (event.metrics) { - for (const [key, value] of Object.entries(event.metrics)) { - if (typeof key !== "string") { - throw new Error("metric keys must be strings"); - } - - if (value !== undefined && typeof value !== "number") { - throw new Error("metric values must be numbers"); - } - } - } - - if ("tags" in event && event.tags) { - validateTags(event.tags); - } - - return { ...event }; -} - -/** - * Creates a deep copy of the given event. Replaces references to user objects - * with placeholder strings to ensure serializability, except for - * {@link Attachment} and {@link ExternalAttachment} objects, which are preserved - * and not deep-copied. - */ -export function deepCopyEvent>( - event: T, -): T { - const attachments: BaseAttachment[] = []; - const ATTACHMENT_INDEX_KEY = "_bt_internal_saved_attachment_idx"; - const ATTACHMENT_MARKER_KEY = "_bt_internal_saved_attachment_marker"; - const attachmentMarker = ++deepCopyEventMarkerCounter; - - // We both check for serializability and round-trip `event` through JSON in - // order to create a "deep copy". This has the benefit of cutting out any - // reference to user objects when the object is logged asynchronously, so that - // in case the objects are modified, the logging is unaffected. In the future, - // this could be changed to a "real" deep copy so that immutable types (long - // strings) do not have to be copied. - const serialized = JSON.stringify(event, (_k, v) => { - if (v instanceof Error) { - return v.message; - } else if (v instanceof SpanImpl || v instanceof NoopSpan) { - return ``; - } else if (v instanceof Experiment) { - return ``; - } else if (v instanceof Dataset) { - return ``; - } else if (v instanceof Logger) { - return ``; - } else if (v instanceof BaseAttachment) { - const idx = attachments.push(v); - return { - [ATTACHMENT_INDEX_KEY]: idx - 1, - [ATTACHMENT_MARKER_KEY]: attachmentMarker, - }; - } else if (v instanceof ReadonlyAttachment) { - return v.reference; - } - return v; - }); - const x = JSON.parse(serialized, (_k, v) => { - if (isDeepCopyAttachmentPlaceholder(v, attachmentMarker)) { - return attachments[v[ATTACHMENT_INDEX_KEY]]; - } - return v; - }); - return x; -} - -let deepCopyEventMarkerCounter = 0; - -function isDeepCopyAttachmentPlaceholder( - value: unknown, - attachmentMarker: number, -): value is Record< - "_bt_internal_saved_attachment_idx" | "_bt_internal_saved_attachment_marker", - number -> { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return false; - } - - const record = value as Record; - if ( - !Object.hasOwn(record, "_bt_internal_saved_attachment_idx") || - !Object.hasOwn(record, "_bt_internal_saved_attachment_marker") - ) { - return false; - } - - if (Object.keys(record).length !== 2) { - return false; - } - - const attachmentIndex = record._bt_internal_saved_attachment_idx as number; - const marker = record._bt_internal_saved_attachment_marker; - - return ( - Number.isInteger(attachmentIndex) && - attachmentIndex >= 0 && - marker === attachmentMarker - ); -} - -/** - * Helper function for uploading attachments. Recursively extracts `Attachment` - * and `ExternalAttachment` objects and replaces them with their associated - * `AttachmentReference` objects. - * - * @param event The event to filter. Will be modified in-place. - * @param attachments Flat array of extracted attachments (output parameter). - */ -function extractAttachments( - event: Record, - attachments: BaseAttachment[], -): void { - for (const [key, value] of Object.entries(event)) { - if (!value) { - continue; - } - - // Base case: Attachment or ExternalAttachment. - if (value instanceof BaseAttachment) { - attachments.push(value); - event[key] = value.reference; - continue; // Attachment cannot be nested. - } - - // Skip if this is already just a reference (no uploader field) - if (value?.type === BRAINTRUST_ATTACHMENT && value.key && !value.uploader) { - // This is already just a reference, skip it - continue; - } - - // Somewhere in our pipeline we're serializing the attachment and then deserializing it. - // This loses the instanceof identity of the attachment, so we need to recreate it because - // the above instance check doesn't catch it. - if (value?.reference?.type === BRAINTRUST_ATTACHMENT && value?.uploader) { - // This looks like a serialized Attachment object, recreate it properly - const attachment = new Attachment({ - data: value.dataDebugString, - filename: value.reference.filename, - contentType: value.reference.content_type, - }); - attachments.push(attachment); - event[key] = attachment.reference; - continue; - } - - // Base case: non-object. - if (!(value instanceof Object)) { - continue; // Nothing to explore recursively. - } - - // Recursive case: object or array. - extractAttachments(value, attachments); - } -} - -/** - * Recursively hydrates any `AttachmentReference` into `Attachment` by modifying - * the input in-place. - * - * @returns The same event instance as the input. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function enrichAttachments>( - event: T, - state: BraintrustState | undefined, -): T { - for (const [key, value] of Object.entries(event)) { - // Base case: AttachmentReference. - const parsedValue = attachmentReferenceSchema.safeParse(value); - if (parsedValue.success) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any - (event as any)[key] = new ReadonlyAttachment(parsedValue.data, state); - continue; - } - - // Base case: non-object. - if (!(value instanceof Object)) { - continue; - } - - // Recursive case: object or array: - enrichAttachments(value, state); - } - - return event; -} - -/** - * Recursively hydrates any `AttachmentReference` into `Attachment` by modifying - * the input in-place. - * - * @returns The same event instance as the input. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -async function resolveAttachmentsToBase64>( - event: T, - state: BraintrustState | undefined, -): Promise { - for (const [key, value] of Object.entries(event)) { - if (value instanceof ReadonlyAttachment) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any - (event as any)[key] = await value.asBase64Url(); - continue; - } - - // Base case: non-object. - if (!(value instanceof Object)) { - continue; - } - - // Recursive case: object or array: - await resolveAttachmentsToBase64(value, state); - } - - return event; -} - -// Note that this only checks properties that are expected of a complete event. -// validateAndSanitizeExperimentLogPartialArgs should still be invoked (after -// handling special fields like 'id'). -function validateAndSanitizeExperimentLogFullArgs( - event: ExperimentLogFullArgs, -): ExperimentLogFullArgs { - if (!("input" in event)) { - throw new Error("input must be specified"); - } - - if (isEmpty(event.output)) { - throw new Error("output must be specified"); - } - if (isEmpty(event.scores)) { - throw new Error("scores must be specified"); - } - - return event; -} - -export type WithTransactionId = R & { - [TRANSACTION_ID_FIELD]: TransactionId; -}; - -export const DEFAULT_FETCH_BATCH_SIZE = 1000; -export const MAX_BTQL_ITERATIONS = 10000; - -export class ObjectFetcher implements AsyncIterable< - WithTransactionId -> { - private _fetchedData: WithTransactionId[] | undefined = undefined; - - constructor( - private objectType: - | "dataset" - | "experiment" - | "project_logs" - | "playground_logs", - private pinnedVersion: string | undefined, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private mutateRecord?: (r: any) => WithTransactionId, - private _internal_btql?: Record, - private _internalBrainstoreRealtime = true, - ) {} - - public get id(): Promise { - throw new Error("ObjectFetcher subclasses must have an 'id' attribute"); - } - - protected async getState(): Promise { - throw new Error("ObjectFetcher subclasses must have a 'getState' method"); - } - - protected getPinnedVersion(): string | undefined { - return this.pinnedVersion; - } - - protected setPinnedVersion(pinnedVersion: string | undefined): void { - this.pinnedVersion = pinnedVersion; - } - - protected getInternalBtql(): Record | undefined { - return this._internal_btql; - } - - private async *fetchRecordsFromApi( - batchSize: number | undefined, - ): AsyncGenerator> { - const state = await this.getState(); - const objectId = await this.id; - const batchLimit = batchSize ?? DEFAULT_FETCH_BATCH_SIZE; - const internalLimit = getInternalBtqlLimit(this._internal_btql); - let remainingLimit = internalLimit; - const internalBtqlWithoutReservedQueryKeys = Object.fromEntries( - Object.entries(this._internal_btql ?? {}).filter( - ([key]) => - key !== "cursor" && - key !== "limit" && - key !== "select" && - key !== "from", - ), - ); - let cursor = undefined; - let iterations = 0; - while (true) { - if (remainingLimit !== undefined && remainingLimit <= 0) { - return; - } - const limit = - remainingLimit === undefined - ? batchLimit - : Math.min(batchLimit, remainingLimit); - const resp = await state.apiConn().post( - `btql`, - { - query: { - select: [ - { - op: "star", - }, - ], - from: { - op: "function", - name: { - op: "ident", - name: [this.objectType], - }, - args: [ - { - op: "literal", - value: objectId, - }, - ], - }, - cursor, - limit, - ...internalBtqlWithoutReservedQueryKeys, - }, - use_columnstore: false, - brainstore_realtime: this._internalBrainstoreRealtime, - query_source: `js_sdk_object_fetcher_${this.objectType}`, - ...(this.pinnedVersion !== undefined - ? { - version: this.pinnedVersion, - } - : {}), - }, - { headers: { "Accept-Encoding": "gzip" } }, - BTQL_HTTP_RETRIES, - ); - const respJson = await resp.json(); - const mutate = this.mutateRecord; - for (const record of respJson.data ?? []) { - if (remainingLimit !== undefined && remainingLimit <= 0) { - return; - } - const mutatedRecord = mutate - ? mutate(record) - : (record as WithTransactionId); - if (remainingLimit !== undefined) { - remainingLimit--; - } - yield mutatedRecord; - } - if (!respJson.cursor) { - break; - } - cursor = respJson.cursor; - iterations++; - if (iterations > MAX_BTQL_ITERATIONS) { - throw new Error("Too many BTQL iterations"); - } - } - } - - /** - * Fetch all records from the object. - * - * @param options Optional parameters for fetching. - * @param options.batchSize The number of records to fetch per request. Defaults to 1000. - * @returns An async generator of records. - */ - async *fetch(options?: { - batchSize?: number; - }): AsyncGenerator> { - if (this._fetchedData !== undefined) { - for (const record of this._fetchedData) { - yield record; - } - return; - } - - for await (const record of this.fetchRecordsFromApi(options?.batchSize)) { - yield record; - } - } - - [Symbol.asyncIterator](): AsyncIterator> { - return this.fetch(); - } - - async fetchedData(options?: { batchSize?: number }) { - if (this._fetchedData === undefined) { - const data: WithTransactionId[] = []; - for await (const record of this.fetchRecordsFromApi(options?.batchSize)) { - data.push(record); - } - this._fetchedData = data; - } - return this._fetchedData || []; - } - - clearCache() { - this._fetchedData = undefined; - } - - public async version(options?: { batchSize?: number }) { - if (this.pinnedVersion !== undefined) { - return this.pinnedVersion; - } else { - let maxVersion: string | undefined = undefined; - for await (const record of this.fetch(options)) { - const xactId = String(record[TRANSACTION_ID_FIELD] ?? "0"); - if (maxVersion === undefined || xactId > maxVersion) { - maxVersion = xactId; - } - } - return maxVersion; - } - } -} - -export type BaseMetadata = Record | void; -export type DefaultMetadataType = void; -export type EvalCaseOrigin = ObjectReference; -export type EvalCase = { - input: Input; - tags?: string[]; - // These fields are only set if the EvalCase is part of a Dataset. - id?: string; - _xact_id?: TransactionId; - created?: string | null; - origin?: EvalCaseOrigin; - // This field is used to help re-run a particular experiment row. - upsert_id?: string; - // The number of times to run the evaluator for this specific input. - trialCount?: number; -} & (Expected extends void ? object : { expected: Expected }) & - (Metadata extends void ? object : { metadata: Metadata }); - -/** - * An experiment is a collection of logged events, such as model inputs and outputs, which represent - * a snapshot of your application at a particular point in time. An experiment is meant to capture more - * than just the model you use, and includes the data you use to test, pre- and post- processing code, - * comparison metrics (scores), and any other metadata you want to include. - * - * Experiments are associated with a project, and two experiments are meant to be easily comparable via - * their `inputs`. You can change the attributes of the experiments in a project (e.g. scoring functions) - * over time, simply by changing what you log. - * - * You should not create `Experiment` objects directly. Instead, use the `braintrust.init()` method. - */ -export class Experiment - extends ObjectFetcher - implements Exportable -{ - private readonly lazyMetadata: LazyValue; - public readonly dataset?: AnyDataset; - private lastStartTime: number; - private lazyId: LazyValue; - private calledStartSpan: boolean; - private state: BraintrustState; - - // For type identification. - public kind = "experiment" as const; - - constructor( - state: BraintrustState, - lazyMetadata: LazyValue, - dataset?: AnyDataset, - ) { - super("experiment", undefined, (r) => enrichAttachments(r, state)); - this.lazyMetadata = lazyMetadata; - this.dataset = dataset; - this.lastStartTime = getCurrentUnixTimestamp(); - this.lazyId = new LazyValue(() => this.id); - this.calledStartSpan = false; - this.state = state; - } - - public get id(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).experiment.id; - })(); - } - - public get loggingState(): BraintrustState { - return this.state; - } - - /** - * Wait for the experiment ID to be resolved. This is useful for ensuring the ID - * is available synchronously in child spans (for OTEL parent attributes). - * @internal - */ - public async _waitForId(): Promise { - await this.lazyId.get().catch(() => { - // Ignore errors - }); - } - - public get name(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).experiment.name; - })(); - } - - public get project(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).project; - })(); - } - - public async _getBaseExperimentId(): Promise { - const baseExperimentId = (await this.lazyMetadata.get()).experiment - .fullInfo["base_exp_id"]; - return typeof baseExperimentId === "string" && baseExperimentId - ? baseExperimentId - : undefined; - } - - private parentObjectType() { - return SpanObjectTypeV3.EXPERIMENT; - } - - protected async getState(): Promise { - // Ensure the login state is populated by awaiting lazyMetadata. - await this.lazyMetadata.get(); - return this.state; - } - - /** - * Log a single event to the experiment. The event will be batched and uploaded behind the scenes. - * - * @param event The event to log. - * @param event.input: The arguments that uniquely define a test case (an arbitrary, JSON serializable object). Later on, Braintrust will use the `input` to know whether two test cases are the same between experiments, so they should not contain experiment-specific state. A simple rule of thumb is that if you run the same experiment twice, the `input` should be identical. - * @param event.output: The output of your application, including post-processing (an arbitrary, JSON serializable object), that allows you to determine whether the result is correct or not. For example, in an app that generates SQL queries, the `output` should be the _result_ of the SQL query generated by the model, not the query itself, because there may be multiple valid queries that answer a single question. - * @param event.expected: (Optional) The ground truth value (an arbitrary, JSON serializable object) that you'd compare to `output` to determine if your `output` value is correct or not. Braintrust currently does not compare `output` to `expected` for you, since there are so many different ways to do that correctly. Instead, these values are just used to help you navigate your experiments while digging into analyses. However, we may later use these values to re-score outputs or fine-tune your models. - * @param event.error: (Optional) The error that occurred, if any. If you use tracing to run an experiment, errors are automatically logged when your code throws an exception. - * @param event.scores: A dictionary of numeric values (between 0 and 1) to log. The scores should give you a variety of signals that help you determine how accurate the outputs are compared to what you expect and diagnose failures. For example, a summarization app might have one score that tells you how accurate the summary is, and another that measures the word similarity between the generated and grouth truth summary. The word similarity score could help you determine whether the summarization was covering similar concepts or not. You can use these scores to help you sort, filter, and compare experiments. - * @param event.metadata: (Optional) a dictionary with additional data about the test example, model outputs, or just about anything else that's relevant, that you can use to help find and analyze examples later. For example, you could log the `prompt`, example's `id`, or anything else that would be useful to slice/dice later. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. - * @param event.metrics: (Optional) a dictionary of metrics to log. The following keys are populated automatically: "start", "end". - * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you. - * @param event.dataset_record_id: (Optional) the id of the dataset record that this event is associated with. This field is required if and only if the experiment is associated with a dataset. This field is unused and will be removed in a future version. - * @param options Additional logging options - * @param options.allowConcurrentWithSpans in rare cases where you need to log at the top level separately from spans on the experiment elsewhere, set this to true. - * @returns The `id` of the logged event. - */ - public log( - event: Readonly, - options?: { allowConcurrentWithSpans?: boolean }, - ): string { - if (this.calledStartSpan && !options?.allowConcurrentWithSpans) { - throw new Error( - "Cannot run toplevel `log` method while using spans. To log to the span, call `experiment.traced` and then log with `span.log`", - ); - } - - event = validateAndSanitizeExperimentLogFullArgs(event); - const span = this.startSpanImpl({ startTime: this.lastStartTime, event }); - this.lastStartTime = span.end(); - return span.id; - } - - /** - * Create a new toplevel span underneath the experiment. The name defaults to "root". - * - * See {@link Span.traced} for full details. - */ - public traced( - callback: (span: Span) => R, - args?: StartSpanArgs & SetCurrentArg, - ): R { - const { setCurrent, ...argsRest } = args ?? {}; - const span = this.startSpan(argsRest); - - return runCatchFinally( - () => { - if (setCurrent ?? true) { - return withCurrent(span, callback); - } else { - return callback(span); - } - }, - (error) => { - logError(span, error); - throw error; - }, - () => span.end(), - ); - } - - /** - * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations - * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", - * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead. - * - * See {@link traced} for full details. - */ - public startSpan(args?: StartSpanArgs): Span { - this.calledStartSpan = true; - return this.startSpanImpl(args); - } - - private startSpanImpl(args?: StartSpanArgs): Span { - return new SpanImpl({ - ...args, - // Sometimes `args` gets passed directly into this function, and it contains an undefined value for `state`. - // To ensure that we always use this experiment's state, we override the `state` argument no matter what. - state: this.state, - ...startSpanParentArgs({ - state: this.state, - parent: args?.parent, - parentObjectType: this.parentObjectType(), - parentObjectId: this.lazyId, - parentComputeObjectMetadataArgs: undefined, - parentSpanIds: undefined, - propagatedEvent: args?.propagatedEvent, - }), - defaultRootType: SpanTypeAttribute.EVAL, - }); - } - - public async fetchBaseExperiment() { - const state = await this.getState(); - const conn = state.appConn(); - - try { - const resp = await conn.post("/api/base_experiment/get_id", { - id: await this.id, - }); - - const base = await resp.json(); - return { - id: base["base_exp_id"], - name: base["base_exp_name"], - }; - } catch (e) { - if (e instanceof FailedHTTPResponse && e.status === 400) { - // No base experiment - return null; - } else { - throw e; - } - } - } - - /** - * Summarize the experiment, including the scores (compared to the closest reference experiment) and metadata. - * - * @param options Options for summarizing the experiment. - * @param options.summarizeScores Whether to summarize the scores. If False, only the metadata will be returned. - * @param options.comparisonExperimentId The experiment to compare against. If None, the most recent experiment on the origin's main branch will be used. - * @returns A summary of the experiment, including the scores (compared to the closest reference experiment) and metadata. - */ - public async summarize( - options: { - readonly summarizeScores?: boolean; - readonly comparisonExperimentId?: string; - } = {}, - ): Promise { - const { - summarizeScores = true, - comparisonExperimentId: comparisonExperimentIdOpt, - } = options || {}; - let comparisonExperimentId = comparisonExperimentIdOpt; - - const state = await this.getState(); - const projectUrl = `${state.appPublicUrl}/app/${encodeURIComponent( - state.orgName!, - )}/p/${encodeURIComponent((await this.project).name)}`; - const experimentUrl = `${projectUrl}/experiments/${encodeURIComponent( - await this.name, - )}`; - - let scores: Record | undefined = undefined; - let metrics: Record | undefined = undefined; - let comparisonExperimentName = undefined; - if (summarizeScores) { - await this.flush(); - if (comparisonExperimentId === undefined) { - const baseExperiment = await this.fetchBaseExperiment(); - if (baseExperiment !== null) { - comparisonExperimentId = baseExperiment.id; - comparisonExperimentName = baseExperiment.name; - } - } else { - try { - const comparisonExperiment = await state - .apiConn() - .get_json(`v1/experiment/${comparisonExperimentId}`); - if (typeof comparisonExperiment["name"] === "string") { - comparisonExperimentName = comparisonExperiment["name"]; - } - } catch { - // If the explicit comparison name lookup fails, still summarize by ID. - } - } - - try { - const results = await state.apiConn().get_json( - "/experiment-comparison2", - { - experiment_id: await this.id, - base_experiment_id: comparisonExperimentId, - }, - 3, - ); - - scores = results["scores"]; - metrics = results["metrics"]; - } catch (e) { - debugLogger - .forState(state) - .warn( - `Failed to fetch experiment scores and metrics: ${e}\n\nView complete results in Braintrust or run experiment.summarize() again.`, - ); - scores = {}; - metrics = {}; - } - } - - return { - projectName: (await this.project).name, - experimentName: await this.name, - projectId: (await this.project).id, - experimentId: await this.id, - projectUrl: projectUrl, - experimentUrl: experimentUrl, - comparisonExperimentName: comparisonExperimentName, - scores: scores ?? {}, - metrics: metrics ?? {}, - }; - } - - /** - * Log feedback to an event in the experiment. Feedback is used to save feedback scores, set an expected value, or add a comment. - * - * @param event - * @param event.id The id of the event to log feedback for. This is the `id` returned by `log` or accessible as the `id` field of a span. - * @param event.scores (Optional) a dictionary of numeric values (between 0 and 1) to log. These scores will be merged into the existing scores for the event. - * @param event.expected (Optional) the ground truth value (an arbitrary, JSON serializable object) that you'd compare to `output` to determine if your `output` value is correct or not. - * @param event.comment (Optional) an optional comment string to log about the event. - * @param event.metadata (Optional) a dictionary with additional data about the feedback. If you have a `user_id`, you can log it here and access it in the Braintrust UI. Note, this metadata does not correspond to the main event itself, but rather the audit log attached to the event. - * @param event.source (Optional) the source of the feedback. Must be one of "external" (default), "app", or "api". - */ - public logFeedback(event: LogFeedbackFullArgs): void { - logFeedbackImpl(this.state, this.parentObjectType(), this.lazyId, event); - } - - /** - * Update a span in the experiment using its id. It is important that you only update a span once the original span has been fully written and flushed, - * since otherwise updates to the span may conflict with the original span. - * - * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields. - */ - public updateSpan( - event: Omit, "id"> & - Required>, - ): void { - const { id, root_span_id, span_id, ...eventRest } = event; - if (!id) { - throw new Error("Span id is required to update a span"); - } - updateSpanImpl({ - state: this.state, - parentObjectType: this.parentObjectType(), - parentObjectId: this.lazyId, - id, - root_span_id, - span_id, - event: eventRest, - }); - } - - /** - * Return a serialized representation of the experiment that can be used to start subspans in other places. - * - * See {@link Span.startSpan} for more details. - */ - public async export(): Promise { - return new (getSpanComponentsClass())({ - object_type: this.parentObjectType(), - object_id: await this.id, - }).toStr(); - } - - /** - * Return this experiment's Braintrust parent string (`experiment_id:`) for - * the `braintrust.parent` baggage entry, or undefined when it cannot be - * determined synchronously. - */ - public _getOtelParent(): string | undefined { - const id = this.lazyId.getSync().value; - return id ? `experiment_id:${id}` : undefined; - } - - /** - * Flush any pending rows to the server. - */ - async flush(): Promise { - return await this.state.bgLogger().flush(); - } -} - -/** - * A read-only view of an experiment, initialized by passing `open: true` to `init()`. - */ -export class ReadonlyExperiment extends ObjectFetcher { - constructor( - private state: BraintrustState, - private readonly lazyMetadata: LazyValue, - ) { - super("experiment", undefined, (r) => enrichAttachments(r, state)); - } - - public get id(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).experiment.id; - })(); - } - - public get name(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).experiment.name; - })(); - } - - public get loggingState(): BraintrustState { - return this.state; - } - - protected async getState(): Promise { - // Ensure the login state is populated by awaiting lazyMetadata. - await this.lazyMetadata.get(); - return this.state; - } - - public async *asDataset< - Input, - Expected, - Metadata = DefaultMetadataType, - >(options?: { - batchSize?: number; - }): AsyncGenerator> { - const records = this.fetch(options); - - for await (const record of records) { - const isRoot = record.is_root ?? record.root_span_id === record.span_id; - if (!isRoot) { - continue; - } - - const { output, expected: expectedRecord, metadata } = record; - const expected = (expectedRecord ?? output) as Expected; - - // Note: We always include expected and metadata fields to maintain type signature alignment. - // This ensures that when the type signature includes `| null | undefined`, the fields - // are still present in the runtime object. While this may incorrectly include fields - // when Metadata/Expected = void, it's preferable to incorrectly excluding them when - // the type signature expects them to be present. - yield { - input: record.input as Input, - tags: record.tags, - expected: expected as Expected, - metadata: metadata as Metadata, - } as unknown as EvalCase; - } - } -} - -let executionCounter = 0; - -export function newId() { - return uuidv4(); -} - -interface ResolvedSpanIds { - spanId: string; - rootSpanId: string; - spanParents: string[] | undefined; -} - -/** - * Resolve all span IDs (span_id, root_span_id, span_parents) from explicit values, parent info, or context. - * - * @param spanId - Optional explicit span_id (from public API) - * @param parentSpanIds - Optional explicit parent span IDs (from parent string or parent span) - * @param lookupSpanParent - Whether to look up parent from context manager if no explicit parent. - * - True (default): start_span() inherits parent/root ids from the active span (if it exists) - * - False: don't look up parent in context (e.g. logger.log() doesn't inherit context) - * @param idGenerator - ID generator for creating new span/trace IDs - * @param contextManager - Context manager for looking up parent spans - * @returns ResolvedSpanIds with resolved span_id, root_span_id, and span_parents - */ -function _resolveSpanIds( - spanId: string | undefined, - parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined, - lookupSpanParent: boolean, - idGenerator: IDGenerator, - contextManager: ContextManager, -): ResolvedSpanIds { - // Generate span_id if not provided - const resolvedSpanId = spanId ?? idGenerator.getSpanId(); - - // If we have explicit parent span ids, use them. - if (parentSpanIds) { - return { - spanId: resolvedSpanId, - rootSpanId: parentSpanIds.rootSpanId, - spanParents: - "parentSpanIds" in parentSpanIds - ? parentSpanIds.parentSpanIds - : [parentSpanIds.spanId], - }; - } - - // If we're using the context manager, see if there's an active parent span. - if (lookupSpanParent) { - const parentInfo = contextManager.getParentSpanIds(); - if (parentInfo) { - return { - spanId: resolvedSpanId, - rootSpanId: parentInfo.rootSpanId, - spanParents: parentInfo.spanParents, - }; - } - } - - // No parent - create new root span - // Root span ID behavior differs between UUID and OTEL generators: - // - UUID (legacy): root_span_id === span_id for backwards compatibility - // - OTEL: root_span_id is a separate trace ID, following OpenTelemetry convention - // where trace_id (root_span_id) represents the entire trace, distinct from - // the individual span's ID - let resolvedRootSpanId: string; - if (idGenerator.shareRootSpanId()) { - resolvedRootSpanId = resolvedSpanId; // Backwards compat for UUID mode - } else { - resolvedRootSpanId = idGenerator.getTraceId(); - } - - return { - spanId: resolvedSpanId, - rootSpanId: resolvedRootSpanId, - spanParents: undefined, - }; -} - -/** - * Primary implementation of the `Span` interface. See {@link Span} for full details on each method. - * - * We suggest using one of the various `traced` methods, instead of creating Spans directly. See {@link Span.startSpan} for full details. - */ -export class SpanImpl implements Span { - /** @internal Snapshot inherited by descendants and delayed instrumentation. */ - public readonly [CAPTURE_ATTACHMENTS]: boolean; - private _state: BraintrustState; - - private isMerge: boolean; - private loggedEndTime: number | undefined; - private propagatedEvent: StartSpanEventArgs | undefined; - - // For internal use only. - private parentObjectType: SpanObjectTypeV3; - private parentObjectId: LazyValue; - private parentComputeObjectMetadataArgs: Record | undefined; - - private _id: string; - private _spanId: string; - private _rootSpanId: string; - private _spanParents: string[] | undefined; - - // Inbound W3C trace-context state (tracestate + raw traceparent flags) to - // forward on outbound propagation. Captured at the span that received it (via - // extractTraceContextFromHeaders) and inherited by all subspans, so that any - // inject() within the trace re-emits the upstream state unchanged, per the W3C - // Trace Context spec. Not interpreted. - private _propagatedState: PropagatedState | undefined; - - public kind = "span" as const; - - constructor( - args: { - state: BraintrustState; - parentObjectType: SpanObjectTypeV3; - parentObjectId: LazyValue; - parentComputeObjectMetadataArgs: Record | undefined; - parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined; - defaultRootType?: SpanType; - spanId?: string; - propagatedState?: PropagatedState | undefined; - [CAPTURE_ATTACHMENTS]?: boolean; - } & Omit & - InitialSpanWriteAsMergeArg & - InternalSpanContextArg, - ) { - this._state = args.state; - this[CAPTURE_ATTACHMENTS] = - args[CAPTURE_ATTACHMENTS] ?? - args.state.captureAttachments ?? - captureAttachmentsFromEnv(); - this._propagatedState = args.propagatedState; - const instrumentationName = - getSpanInstrumentationName(args) ?? - INSTRUMENTATION_NAMES.BRAINTRUST_JS_LOGGER; - - const spanAttributes = args.spanAttributes ?? {}; - const rawEvent = args.event ?? {}; - const type = - args.type ?? (args.parentSpanIds ? undefined : args.defaultRootType); - - this.loggedEndTime = undefined; - this.parentObjectType = args.parentObjectType; - this.parentObjectId = args.parentObjectId; - this.parentComputeObjectMetadataArgs = args.parentComputeObjectMetadataArgs; - - // Merge propagatedEvent into event. The propagatedEvent data will get - // propagated-and-merged into every subspan. - this.propagatedEvent = args.propagatedEvent; - if (this.propagatedEvent) { - mergeDicts(rawEvent, this.propagatedEvent); - } - const { id: eventId, ...event } = rawEvent; - - const callerLocation = iso.getCallerLocation(); - const name = (() => { - if (args.name) return args.name; - if (!args.parentSpanIds) return "root"; - if (callerLocation) { - const pathComponents = callerLocation.caller_filename.split("/"); - const filename = pathComponents[pathComponents.length - 1]; - return [callerLocation.caller_functionname] - .concat( - filename ? [`${filename}:${callerLocation.caller_lineno}`] : [], - ) - .join(":"); - } - return "subspan"; - })(); - - const internalData = { - metrics: { - start: args.startTime ?? getCurrentUnixTimestamp(), - }, - context: mergeSpanOriginContext( - { ...callerLocation, ...args[INTERNAL_SPAN_CONTEXT] }, - instrumentationName, - this._state.spanOriginEnvironment, - ), - span_attributes: { - name, - type, - ...spanAttributes, - exec_counter: executionCounter++, - }, - created: new Date().toISOString(), - }; - - this._id = eventId ?? this._state.idGenerator.getSpanId(); - - // Resolve span IDs using the helper function that matches Python's _resolve_span_ids - const resolvedIds = _resolveSpanIds( - args.spanId, - args.parentSpanIds, - true, // Always lookup span parent from context manager unless explicit parent provided - this._state.idGenerator, - this._state.contextManager, - ); - - this._spanId = resolvedIds.spanId; - this._rootSpanId = resolvedIds.rootSpanId; - this._spanParents = resolvedIds.spanParents; - - // Deterministic spans can be initialized concurrently by separate - // workflow executions, so their first write must not replace later merges. - this.isMerge = args[INITIAL_SPAN_WRITE_AS_MERGE] === true; - if (!args[RESUME_SPAN_WITHOUT_INITIAL_WRITE]) { - this.logInternal({ event, internalData }); - } - this.isMerge = true; - } - - public getParentInfo() { - return { - objectType: this.parentObjectType, - objectId: this.parentObjectId, - computeObjectMetadataArgs: this.parentComputeObjectMetadataArgs && { - ...this.parentComputeObjectMetadataArgs, - }, - }; - } - - public get id(): string { - return this._id; - } - - public get spanId(): string { - return this._spanId; - } - - public get rootSpanId(): string { - return this._rootSpanId; - } - - public get spanParents(): string[] { - return this._spanParents ?? []; - } - - public setAttributes(args: Omit): void { - this.logInternal({ internalData: { span_attributes: args } }); - } - - public setSpanParents(parents: string[]): void { - this.logInternal({ internalData: { span_parents: parents } }); - } - - public log(event: ExperimentLogPartialArgs): void { - this.logInternal({ event }); - } - - private logInternal({ - event, - internalData, - }: { - event?: ExperimentLogPartialArgs; - // `internalData` contains fields that are not part of the "user-sanitized" - // set of fields which we want to log in just one of the span rows. - internalData?: Partial; - }): void { - const [serializableInternalData, lazyInternalData] = splitLoggingData({ - event, - internalData, - }); - - // Deep copy mutable user data. - const partialRecord = deepCopyEvent({ - id: this.id, - span_id: this._spanId, - root_span_id: this._rootSpanId, - span_parents: this._spanParents, - ...serializableInternalData, - [IS_MERGE_FIELD]: this.isMerge, - }); - - if (typeof partialRecord.metrics?.end === "number") { - this.loggedEndTime = partialRecord.metrics.end; - } - - // Write to local span cache for scorer access - // Only cache experiment spans - regular logs don't need caching - if (this.parentObjectType === SpanObjectTypeV3.EXPERIMENT) { - const cachedSpan: CachedSpan = { - input: partialRecord.input, - output: partialRecord.output, - expected: partialRecord.expected, - error: partialRecord.error, - scores: partialRecord.scores, - metrics: partialRecord.metrics, - metadata: partialRecord.metadata, - tags: partialRecord.tags, - span_id: this._spanId, - span_parents: this._spanParents, - is_root: !this._spanParents || this._spanParents.length === 0, - span_attributes: partialRecord.span_attributes, - }; - this._state.spanCache.queueWrite( - this._rootSpanId, - this._spanId, - cachedSpan, - ); - } - - const computeRecord = async () => { - const record = { - ...partialRecord, - ...Object.fromEntries( - await Promise.all( - Object.entries(lazyInternalData).map(async ([key, value]) => [ - key, - await value.get(), - ]), - ), - ), - ...new SpanComponentsV3({ - object_type: this.parentObjectType, - object_id: await this.parentObjectId.get(), - }).objectIdFields(), - }; - // Customize inside the memoized lazy value, before attachment processing, - // merging, and masking. Retries reuse the customized record or drop result. - return customizeSpanExport(record) as BackgroundLogEvent | null; - }; - this._state.bgLogger().log([new LazyValue(computeRecord)]); - } - - public logFeedback(event: Omit): void { - logFeedbackImpl(this._state, this.parentObjectType, this.parentObjectId, { - ...event, - id: this.id, - }); - } - - public traced( - callback: (span: Span) => R, - args?: StartSpanArgs & SetCurrentArg, - ): R { - const { setCurrent, ...argsRest } = args ?? {}; - const span = this.startSpan(argsRest); - return runCatchFinally( - () => { - if (setCurrent ?? true) { - return withCurrent(span, callback); - } else { - return callback(span); - } - }, - (error) => { - logError(span, error); - throw error; - }, - () => span.end(), - ); - } - - public startSpan(args?: StartSpanArgs): Span { - const parentSpanIds: ParentSpanIds | undefined = args?.parent - ? undefined - : { spanId: this._spanId, rootSpanId: this._rootSpanId }; - return new SpanImpl({ - state: this._state, - [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], - ...args, - ...startSpanParentArgs({ - state: this._state, - parent: args?.parent, - parentObjectType: this.parentObjectType, - parentObjectId: this.parentObjectId, - parentComputeObjectMetadataArgs: this.parentComputeObjectMetadataArgs, - parentSpanIds, - propagatedEvent: args?.propagatedEvent ?? this.propagatedEvent, - propagatedState: this._propagatedState, - }), - }); - } - - public startSpanWithParents( - spanId: string, - spanParents: string[], - args?: StartSpanArgs, - ): Span { - const parentSpanIds: MultiParentSpanIds = { - parentSpanIds: spanParents, - rootSpanId: this._rootSpanId, - }; - return new SpanImpl({ - state: this._state, - [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], - ...args, - ...startSpanParentArgs({ - state: this._state, - parent: args?.parent, - parentObjectType: this.parentObjectType, - parentObjectId: this.parentObjectId, - parentComputeObjectMetadataArgs: this.parentComputeObjectMetadataArgs, - parentSpanIds, - propagatedEvent: args?.propagatedEvent ?? this.propagatedEvent, - propagatedState: this._propagatedState, - }), - spanId, - }); - } - - public end(args?: EndSpanArgs): number { - let endTime: number; - let internalData: Partial = {}; - if (!this.loggedEndTime) { - endTime = args?.endTime ?? getCurrentUnixTimestamp(); - internalData = { metrics: { end: endTime } }; - } else { - endTime = this.loggedEndTime; - } - this.logInternal({ internalData }); - return endTime; - } - - public async export(): Promise { - // Disable span cache since remote function spans won't be in the local cache - this._state.spanCache.disable(); - - return new (getSpanComponentsClass())({ - object_type: this.parentObjectType, - - ...(this.parentComputeObjectMetadataArgs && - !this.parentObjectId.hasSucceeded - ? { compute_object_metadata_args: this.parentComputeObjectMetadataArgs } - : { object_id: await this.parentObjectId.get() }), - row_id: this.id, - span_id: this._spanId, - root_span_id: this._rootSpanId, - propagated_event: this.propagatedEvent, - }).toStr(); - } - - /** - * Return this span's Braintrust parent string (`project_id:`, - * `project_name:`, or `experiment_id:`) for the `braintrust.parent` - * baggage entry, or undefined when it cannot be determined synchronously. - */ - public _getOtelParent(): string | undefined { - if (this.parentObjectType === SpanObjectTypeV3.PROJECT_LOGS) { - const id = - this.parentComputeObjectMetadataArgs?.project_id || - this.parentObjectId.getSync().value; - const name = this.parentComputeObjectMetadataArgs?.project_name; - if (id) { - return `project_id:${id}`; - } else if (name) { - return `project_name:${name}`; - } - } else if (this.parentObjectType === SpanObjectTypeV3.EXPERIMENT) { - const id = - this.parentComputeObjectMetadataArgs?.experiment_id || - this.parentObjectId.getSync().value; - if (id) { - return `experiment_id:${id}`; - } - } - return undefined; - } - - public inject(): Record; - public inject(carrier: T): T; - public inject( - carrier?: T, - ): T | Record { - const resolvedCarrier = carrier ?? {}; - try { - const braintrustParent = - this._getOtelParent() ?? this._propagatedState?.braintrustParent; - if (!braintrustParent) { - // Symmetric with the receive-side warning in resolveW3cParent: - // surface this in the process that caused it, rather than leaving the - // consumer to debug a trace that arrived with no destination. - debugLogger - .forState(this._state) - .warn( - "Injecting trace context without braintrust.parent because the span's " + - "destination is not available yet. The receiver will start a new " + - "local trace instead of continuing this one.", - ); - } - _injectIntoCarrier(resolvedCarrier, { - traceId: this._rootSpanId, - spanId: this._spanId, - braintrustParent, - propagatedState: this._propagatedState, - }); - } catch (e) { - // best-effort: never break the caller - debugLogger.warn(`Error injecting trace context: ${e}`); - } - return resolvedCarrier; - } - - public async permalink(): Promise { - return await permalink(await this.export(), { - state: this._state, - }); - } - - public link(): string { - if (!this.id) { - return NOOP_SPAN_PERMALINK; - } - - try { - let baseUrl: string | null = null; - - if (this.parentObjectType === SpanObjectTypeV3.PROJECT_LOGS) { - // For PROJECT_LOGS, use Logger's _getLinkBaseUrl which checks - // state -> linkArgs -> env var (matches Python) - const curLogger = this._state.currentLogger; - if (curLogger) { - baseUrl = curLogger._getLinkBaseUrl(); - } - } - - // For EXPERIMENT or if Logger not available, fall back to state -> env var - if (!baseUrl) { - baseUrl = _getLinkBaseUrl(this._state); - if (!baseUrl) { - throw new Error("log-in-or-provide-org-name"); - } - } - - return this._link(baseUrl); - } catch (e) { - return getErrPermlink(e instanceof Error ? e.message : String(e)); - } - } - - _link(baseUrl: string): string { - // NOTE[matt]: I believe lazy values should not exist in the span or the logger. - // Nothing in this module should have the possibility of blocking with the lone exception of - // flush() which should be a clear exception. We shouldn't build on it and - // plan to remove it in the future. - const args = this.parentComputeObjectMetadataArgs; - - switch (this.parentObjectType) { - case SpanObjectTypeV3.PROJECT_LOGS: { - // Links to spans require a project id or name. We might not either, so use whatever - // we can to make a link without making a roundtrip to the server. - const projectID = - args?.project_id || this.parentObjectId.getSync().value; - const projectName = args?.project_name; - if (projectID) { - return `${baseUrl}/object?object_type=project_logs&object_id=${projectID}&id=${this._id}`; - } else if (projectName) { - return `${baseUrl}/p/${projectName}/logs?oid=${this._id}`; - } else { - return getErrPermlink("provide-project-name-or-id"); - } - } - case SpanObjectTypeV3.EXPERIMENT: { - // Experiment links require an id, so the sync version will only work after the experiment is - // resolved. - const expID = - args?.experiment_id || this.parentObjectId?.getSync()?.value; - if (!expID) { - return getErrPermlink("provide-experiment-id"); - } else { - return `${baseUrl}/object?object_type=experiment&object_id=${expID}&id=${this._id}`; - } - } - case SpanObjectTypeV3.PLAYGROUND_LOGS: { - // FIXME[matt] I dont believe these are used in the SDK. - return NOOP_SPAN_PERMALINK; - } - default: { - // trigger a compile-time error if we add a new object type - const _exhaustive: never = this.parentObjectType; - // eslint-disable-next-line no-unused-expressions - _exhaustive; - return NOOP_SPAN_PERMALINK; - } - } - } - - async flush(): Promise { - return await this._state.bgLogger().flush(); - } - - public state(): BraintrustState { - return this._state; - } - - // Custom inspect for Node.js console.log - [Symbol.for("nodejs.util.inspect.custom")](): string { - return `SpanImpl { - kind: '${this.kind}', - id: '${this.id}', - spanId: '${this.spanId}', - rootSpanId: '${this.rootSpanId}', - spanParents: ${JSON.stringify(this.spanParents)} -}`; - } - - // Custom toString - toString(): string { - return `SpanImpl(id=${this.id}, spanId=${this.spanId})`; - } -} - -function splitLoggingData({ - event, - internalData, -}: { - event?: ExperimentLogPartialArgs; - // `internalData` contains fields that are not part of the "user-sanitized" - // set of fields which we want to log in just one of the span rows. - internalData?: Partial; -}): [Partial, Record>] { - // There should be no overlap between the dictionaries being merged, - // except for `sanitized` and `internalData`, where the former overrides - // the latter. - const sanitized = validateAndSanitizeExperimentLogPartialArgs(event ?? {}); - - const sanitizedAndInternalData: Partial & - Partial = {}; - mergeDicts(sanitizedAndInternalData, internalData || {}); - mergeDicts(sanitizedAndInternalData, sanitized); - - const serializableInternalData: typeof sanitizedAndInternalData = {}; - const lazyInternalData: Record> = {}; - - for (const [key, value] of Object.entries(sanitizedAndInternalData) as [ - keyof typeof sanitizedAndInternalData, - any, - ][]) { - if (value instanceof BraintrustStream) { - const streamCopy = value.copy(); - lazyInternalData[key] = new LazyValue(async () => { - return await new Promise((resolve, reject) => { - streamCopy - .toReadableStream() - .pipeThrough(createFinalValuePassThroughStream(resolve, reject)) - .pipeTo(devNullWritableStream()); - }); - }); - } else if (value instanceof ReadableStream) { - lazyInternalData[key] = new LazyValue(async () => { - return await new Promise((resolve, reject) => { - value - .pipeThrough(createFinalValuePassThroughStream(resolve, reject)) - .pipeTo(devNullWritableStream()); - }); - }); - } else { - serializableInternalData[key] = value; - } - } - - return [serializableInternalData, lazyInternalData]; -} - -/** - * A dataset is a collection of records, such as model inputs and expected outputs, which represent - * data you can use to evaluate and fine-tune models. You can log production data to datasets, - * curate them with interesting examples, edit/delete records, and run evaluations against them. - * - * You should not create `Dataset` objects directly. Instead, use the `braintrust.initDataset()` method. - */ -export class Dataset extends ObjectFetcher { - private readonly lazyMetadata: LazyValue; - private readonly __braintrust_dataset_marker = true; - private newRecords = 0; - private lazyPinnedVersion?: LazyValue; - private pinnedEnvironment?: string; - private pinnedSnapshotName?: string; - - constructor( - private state: BraintrustState, - lazyMetadata: LazyValue, - pinnedVersion?: string, - _internal_btql?: Record, - pinState?: DatasetPinState, - ) { - super( - "dataset", - pinnedVersion, - (r: AnyDatasetRecord) => - ensureNewDatasetRecord( - enrichAttachments(r, this.state), - ) as WithTransactionId, - _internal_btql, - ); - void this.__braintrust_dataset_marker; - this.lazyMetadata = lazyMetadata; - this.lazyPinnedVersion = pinState?.lazyPinnedVersion; - this.pinnedEnvironment = pinState?.pinnedEnvironment; - this.pinnedSnapshotName = pinState?.pinnedSnapshotName; - } - - public get id(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).dataset.id; - })(); - } - - public get name(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).dataset.name; - })(); - } - - public get project(): Promise { - return (async () => { - return (await this.lazyMetadata.get()).project; - })(); - } - - public get loggingState(): BraintrustState { - return this.state; - } - - public async toEvalData(): Promise<{ - dataset_id: string; - dataset_version?: string; - dataset_environment?: string; - dataset_snapshot_name?: string; - _internal_btql?: Record; - }> { - await this.getState(); - const metadata = await this.lazyMetadata.get(); - const pinnedVersion = this.getPinnedVersion(); - const internalBtql = this.getInternalBtql(); - - return { - dataset_id: metadata.dataset.id, - ...(this.pinnedEnvironment !== undefined - ? { - dataset_environment: this.pinnedEnvironment, - } - : {}), - ...(this.pinnedEnvironment === undefined && - this.pinnedSnapshotName !== undefined - ? { - dataset_snapshot_name: this.pinnedSnapshotName, - } - : {}), - ...(this.pinnedEnvironment === undefined && - this.pinnedSnapshotName === undefined && - pinnedVersion !== undefined - ? { - dataset_version: pinnedVersion, - } - : {}), - ...(internalBtql !== undefined ? { _internal_btql: internalBtql } : {}), - }; - } - - protected async getState(): Promise { - // Ensure the login state is populated by awaiting lazyMetadata. - await this.lazyMetadata.get(); - if ( - this.lazyPinnedVersion !== undefined && - this.getPinnedVersion() === undefined - ) { - this.setPinnedVersion(await this.lazyPinnedVersion.get()); - } - return this.state; - } - - public override async version(options?: { batchSize?: number }) { - const pinnedVersion = this.getPinnedVersion(); - if (pinnedVersion !== undefined) { - return pinnedVersion; - } - await this.getState(); - return await super.version(options); - } - - private validateEvent({ - metadata, - tags, - }: { - metadata?: Record; - tags?: string[]; - }) { - if (metadata !== undefined) { - for (const key of Object.keys(metadata)) { - if (typeof key !== "string") { - throw new Error("metadata keys must be strings"); - } - } - } - - if (tags) { - validateTags(tags); - } - } - - private createArgs({ - id, - input, - expected, - metadata, - tags, - origin, - isMerge, - }: { - id: string; - input?: unknown; - expected?: unknown; - metadata?: Record; - tags?: string[]; - origin?: ObjectReference; - isMerge?: boolean; - }): LazyValue { - return new LazyValue(async () => { - const dataset_id = await this.id; - const args: BackgroundLogEvent = { - id, - input, - expected, - tags, - dataset_id, - created: !isMerge ? new Date().toISOString() : undefined, //if we're merging/updating an event we will not add this ts - metadata, - origin, - ...(!!isMerge - ? { - [IS_MERGE_FIELD]: true, - } - : {}), - }; - - return args; - }); - } - - /** - * Insert a single record to the dataset. The record will be batched and uploaded behind the scenes. If you pass in an `id`, - * and a record with that `id` already exists, it will be overwritten (upsert). - * - * @param event The event to log. - * @param event.input The argument that uniquely define an input case (an arbitrary, JSON serializable object). - * @param event.expected The output of your application, including post-processing (an arbitrary, JSON serializable object). - * @param event.tags (Optional) a list of strings that you can use to filter and group records later. - * @param event.metadata (Optional) a dictionary with additional data about the test example, model outputs, or just - * about anything else that's relevant, that you can use to help find and analyze examples later. For example, you could log the - * `prompt`, example's `id`, or anything else that would be useful to slice/dice later. The values in `metadata` can be any - * JSON-serializable type, but its keys must be strings. - * @param event.origin (Optional) a reference to the source object this dataset record was derived from. - * @param event.id (Optional) a unique identifier for the event. If you don't provide one, Braintrust will generate one for you. - * @returns The `id` of the logged record. - */ - public insert({ - input, - expected, - metadata, - tags, - id, - origin, - }: { - readonly input?: unknown; - readonly expected?: unknown; - readonly tags?: string[]; - readonly metadata?: Record; - readonly id?: string; - readonly origin?: ObjectReference; - }): string { - this.validateEvent({ metadata, tags }); - - const rowId = id || uuidv4(); - const args = this.createArgs( - deepCopyEvent({ - id: rowId, - input, - expected, - metadata, - tags, - origin, - isMerge: false, - }), - ); - - this.state.bgLogger().log([args]); - this.newRecords++; - return rowId; - } - - /** - * Update fields of a single record in the dataset. The updated fields will be batched and uploaded behind the scenes. - * You must pass in an `id` of the record to update. Only the fields provided will be updated; other fields will remain unchanged. - * - * @param event The fields to update in the record. - * @param event.id The unique identifier of the record to update. - * @param event.input (Optional) The new input value for the record (an arbitrary, JSON serializable object). - * @param event.expected (Optional) The new expected output value for the record (an arbitrary, JSON serializable object). - * @param event.tags (Optional) A list of strings to update the tags of the record. - * @param event.metadata (Optional) A dictionary to update the metadata of the record. The values in `metadata` can be any - * JSON-serializable type, but its keys must be strings. - * @returns The `id` of the updated record. - */ - public update({ - input, - expected, - metadata, - tags, - id, - }: { - readonly id: string; - readonly input?: unknown; - readonly expected?: unknown; - readonly tags?: string[]; - readonly metadata?: Record; - }): string { - this.validateEvent({ metadata, tags }); - - const args = this.createArgs( - deepCopyEvent({ - id, - input, - expected, - metadata, - tags, - isMerge: true, - }), - ); - - this.state.bgLogger().log([args]); - return id; - } - - public delete(id: string): string { - const args = new LazyValue(async () => ({ - id, - dataset_id: await this.id, - created: new Date().toISOString(), - _object_delete: true, - })); - - this.state.bgLogger().log([args]); - return id; - } - - public async createSnapshot({ - name, - description, - update, - }: { - readonly name: string; - readonly description?: string; - readonly update?: boolean; - }): Promise { - await this.flush(); - const state = await this.getState(); - const datasetId = await this.id; - // Live-row versions omit deletions. After flushing, get a server transaction - // boundary that includes every completed write, even if no live rows remain. - // getState() above also resolves versions pinned by snapshot or environment. - const currentVersion = - this.getPinnedVersion() ?? - (await (await state.apiConn().get("xact-id")).text()); - const response = await state - .appConn() - .post_json("api/dataset_snapshot/register", { - dataset_id: datasetId, - dataset_snapshot_name: name, - description, - xact_id: currentVersion, - update, - }); - return datasetSnapshotRegisterResponseSchema.parse(response) - .dataset_snapshot; - } - - public async listSnapshots(): Promise { - const state = await this.getState(); - return await getDatasetSnapshots({ - state, - datasetId: await this.id, - }); - } - - public async getSnapshot( - lookup: DatasetSnapshotLookup, - ): Promise { - const state = await this.getState(); - const datasetId = await this.id; - return await getDatasetSnapshot({ - state, - datasetId, - ...lookup, - }); - } - - public async updateSnapshot( - snapshotId: string, - { - name, - description, - }: { - readonly name?: string; - readonly description?: string | null; - }, - ): Promise { - const state = await this.getState(); - return datasetSnapshotSchema.parse( - await state.appConn().post_json("api/dataset_snapshot/patch_id", { - id: snapshotId, - name, - description, - }), - ); - } - - public async deleteSnapshot(snapshotId: string): Promise { - const state = await this.getState(); - return datasetSnapshotSchema.parse( - await state.appConn().post_json("api/dataset_snapshot/delete_id", { - id: snapshotId, - }), - ); - } - - public async restorePreview({ - version, - }: { - readonly version: string; - }): Promise { - await this.flush(); - const state = await this.getState(); - const datasetId = await this.id; - return datasetRestorePreviewResultSchema.parse( - await state - .apiConn() - .post_json(`v1/dataset/${datasetId}/restore/preview`, { - version, - }), - ); - } - - public async restore({ - version, - }: { - readonly version: string; - }): Promise { - await this.flush(); - const state = await this.getState(); - const datasetId = await this.id; - return datasetRestoreResultSchema.parse( - await state.apiConn().post_json(`v1/dataset/${datasetId}/restore`, { - version, - }), - ); - } - - /** - * Summarize the dataset, including high level metrics about its size and other metadata. - * @param summarizeData Whether to summarize the data. If false, only the metadata will be returned. - * @returns `DatasetSummary` - * @returns A summary of the dataset. - */ - public async summarize( - options: { readonly summarizeData?: boolean } = {}, - ): Promise { - const { summarizeData = true } = options || {}; - - await this.flush(); - const state = await this.getState(); - const projectUrl = `${state.appPublicUrl}/app/${encodeURIComponent( - state.orgName!, - )}/p/${encodeURIComponent((await this.project).name)}`; - const datasetUrl = `${projectUrl}/datasets/${encodeURIComponent( - await this.name, - )}`; - - let dataSummary: DataSummary | undefined; - if (summarizeData) { - const rawDataSummary = z - .object({ - total_records: z.number(), - }) - .parse( - await state.apiConn().get_json( - "dataset-summary", - { - dataset_id: await this.id, - }, - 3, - ), - ); - dataSummary = { - newRecords: this.newRecords, - totalRecords: rawDataSummary.total_records, - }; - } - - return { - projectName: (await this.project).name, - datasetName: await this.name, - projectUrl, - datasetUrl, - dataSummary, - }; - } - - /** - * Flush any pending rows to the server. - */ - async flush(): Promise { - return await this.state.bgLogger().flush(); - } - - public static isDataset(data: unknown): data is Dataset { - return ( - typeof data === "object" && - data !== null && - "__braintrust_dataset_marker" in data - ); - } -} - -type CompiledPromptResponseFormat = - | { type: "json_object" } - | { type: "text" } - | { - type: "json_schema"; - json_schema: { - name: string; - description?: string; - schema?: Record; - strict?: boolean | null; - }; - }; - -// Keep the compiled API independent of the backend's provider parameter union. -// "none" is excluded from reasoning_effort for compatibility with older OpenAI clients. -export interface CompiledPromptParams { - chat_template_kwargs?: { enable_thinking?: boolean; [key: string]: unknown }; - model: string; - max_tokens: number; - temperature?: number; - top_p?: number; - max_completion_tokens?: number; - frequency_penalty?: number; - presence_penalty?: number; - response_format?: CompiledPromptResponseFormat; - tool_choice?: - | "auto" - | "none" - | "required" - | { - type: "function"; - function: { name: string }; - }; - function_call?: "auto" | "none" | { name: string }; - n?: number; - stop?: string[]; - reasoning_effort?: "minimal" | "low" | "medium" | "high"; - verbosity?: "low" | "medium" | "high"; - top_k?: number; - stop_sequences?: string[]; - reasoning_enabled?: boolean; - reasoning_budget?: number; - /** Legacy parameter; prefer max_tokens. */ - max_tokens_to_sample?: number; - maxOutputTokens?: number; - topP?: number; - topK?: number; -} - -export type ChatPrompt = { - messages: OpenAIMessage[]; - tools?: ChatCompletionTool[]; -}; -export type CompletionPrompt = { - prompt: string; -}; - -export type CompiledPrompt = - CompiledPromptParams & { - span_info?: { - name?: string; - spanAttributes?: Record; - metadata: { - prompt: { - variables: Record; - id: string; - project_id: string; - version: string; - }; - }; - }; - } & (Flavor extends "chat" - ? ChatPrompt - : Flavor extends "completion" - ? CompletionPrompt - : // eslint-disable-next-line @typescript-eslint/no-empty-object-type - {}); - -export interface DefaultPromptArgs - extends - Partial, - Partial, - Partial { - use_cache?: boolean; -} - -function isAttachmentObject(value: unknown): boolean { - return ( - BraintrustAttachmentReferenceSchema.safeParse(value).success || - InlineAttachmentReferenceSchema.safeParse(value).success || - ExternalAttachmentReferenceSchema.safeParse(value).success - ); -} - -// Simple URL validation all braintrust attachment urls or generic links -function isURL(url: string): boolean { - try { - const parsedUrl = new URL(url.trim()); - return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"; - } catch { - return false; - } -} - -/** - * Detect and expand attachment array variables before template rendering. - * Supports both simple variables ({{images}}) and nested paths ({{data.images}}). - * @param content The message content (should be a template variable like "{{images}}" or "{{data.images}}") - * @param variables The variables object with array values - * @returns Array of image_url parts if expansion occurred, null otherwise - */ -function expandAttachmentArrayPreTemplate( - content: unknown, - variables: Record, -): unknown[] | null { - if (typeof content !== "string") return null; - - // Detect {{varName}} or {{nested.path}} pattern (exact match only - no mixed content) - // Supports: {{images}}, {{data.images}}, {{user.profile.images}}, etc. - const match = content.match(/^\{\{\s*([\w.]+)\s*\}\}$/); - if (!match) return null; - - const varPath = match[1]; - - const value = varPath.includes(".") - ? getObjValueByPath(variables, varPath.split(".")) - : variables[varPath]; - - if (!Array.isArray(value)) return null; - - // Check if array contains attachments or image URLs - const allValid = value.every( - (v) => isAttachmentObject(v) || (typeof v === "string" && isURL(v)), - ); - - if (!allValid) return null; - - // Expand directly to image_url parts - return value.map((item) => ({ - type: "image_url" as const, - image_url: { url: item }, - })); -} - -export function renderMessage( - render: (template: string) => string, - message: T, -): T; - -export function renderMessage( - render: (template: string) => string, - message: T, -): T { - return renderMessageImpl(render, message, {}); -} - -export function renderMessageImpl( - render: (template: string) => string, - message: T, - variables?: Record, -): T { - return { - ...message, - ...("content" in message - ? { - content: isEmpty(message.content) - ? undefined - : typeof message.content === "string" - ? render(message.content) - : message.content.flatMap((c) => { - switch (c.type) { - case "text": - return [{ ...c, text: render(c.text) }]; - case "image_url": - if (isObject(c.image_url.url)) { - throw new Error( - "Attachments must be replaced with URLs before calling `build()`", - ); - } - - if (variables) { - const expanded = expandAttachmentArrayPreTemplate( - c.image_url.url, - variables, - ); - if (expanded) { - return expanded; - } - } - - // Otherwise render the URL template normally - return [ - { - ...c, - image_url: { - ...c.image_url, - url: render(c.image_url.url), - }, - }, - ]; - case "input_audio": - return [ - { - ...c, - input_audio: { - ...c.input_audio, - data: render(c.input_audio.data), - }, - }, - ]; - case "file": - return [ - { - ...c, - file: { - ...(c.file.file_data && { - file_data: render(c.file.file_data), - }), - ...(c.file.file_id && { - file_id: render(c.file.file_id), - }), - ...(c.file.filename && { - filename: render(c.file.filename), - }), - }, - }, - ]; - default: - const _exhaustiveCheck: never = c; - return _exhaustiveCheck; - } - }), - } - : {}), - ...("tool_calls" in message - ? { - tool_calls: isEmpty(message.tool_calls) - ? undefined - : message.tool_calls.map((t) => { - return { - type: t.type, - id: render(t.id), - function: { - name: render(t.function.name), - arguments: render(t.function.arguments), - }, - }; - }), - } - : {}), - ...("tool_call_id" in message - ? { - tool_call_id: render(message.tool_call_id), - } - : {}), - }; -} - -interface PromptMetadata { - name: string; - slug: string; - project_id?: string; - description?: string | null; - created?: string | null; - prompt_data?: PromptData | null; - tags?: string[] | null; - // Preserve the generated prompt row's permissive metadata contract. - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - metadata?: {} | null; - function_type?: FunctionTypeEnum | null; -} - -export type PromptRowWithId< - HasId extends boolean = true, - HasVersion extends boolean = true, -> = PromptMetadata & - (HasId extends true ? { id: string } : { id?: string }) & - (HasVersion extends true ? { _xact_id: string } : { _xact_id?: string }); - -export function deserializePlainStringAsJSON(s: string) { - if (s.trim() === "") { - return { value: null, error: undefined }; - } - - try { - return { value: JSON.parse(s), error: undefined }; - } catch (e) { - return { value: s, error: e }; - } -} - -function renderTemplatedObject( - obj: unknown, - args: Record, - options: { strict?: boolean; templateFormat: TemplateFormat }, -): unknown { - if (typeof obj === "string") { - return renderTemplateContent( - obj, - args, - (value) => (typeof value === "string" ? value : JSON.stringify(value)), - { - strict: options.strict, - templateFormat: options.templateFormat, - }, - ); - } else if (isArray(obj)) { - return obj.map((item) => renderTemplatedObject(item, args, options)); - } else if (isObject(obj)) { - return Object.fromEntries( - Object.entries(obj).map(([key, value]) => [ - key, - renderTemplatedObject(value, args, options), - ]), - ); - } - return obj; -} - -export function renderPromptParams( - params: ModelParams | undefined, - args: Record, - options: { strict?: boolean; templateFormat?: TemplateFormat } = {}, -): ModelParams | undefined { - const templateFormat = parseTemplateFormat(options.templateFormat); - const strict = !!options.strict; - - const schemaParsed = z - .object({ - response_format: z.object({ - type: z.literal("json_schema"), - json_schema: responseFormatJsonSchemaSchema - .omit({ schema: true }) - .extend({ - schema: z.unknown(), - }), - }), - }) - .safeParse(params); - if (schemaParsed.success) { - const rawSchema = schemaParsed.data.response_format.json_schema.schema; - const templatedSchema = renderTemplatedObject(rawSchema, args, { - strict, - templateFormat, - }); - const parsedSchema = - typeof templatedSchema === "string" - ? deserializePlainStringAsJSON(templatedSchema).value - : templatedSchema; - - return { - ...params, - response_format: { - ...schemaParsed.data.response_format, - json_schema: { - ...schemaParsed.data.response_format.json_schema, - schema: parsedSchema, - }, - }, - }; - } - return params; -} - -export interface SerializedPrompt { - metadata: PromptRowWithId | PromptSessionEvent; - defaults: DefaultPromptArgs; - noTrace: boolean; -} - -// Initialized by the class so cache serialization can access private fields -// without exposing a serialization method on the public Prompt API. -export let serializePromptForCache: (prompt: Prompt) => SerializedPrompt; - -export class Prompt< - HasId extends boolean = true, - HasVersion extends boolean = true, -> { - private parsedPromptData: PromptData | undefined; - private hasParsedPromptData = false; - private readonly __braintrust_prompt_marker = true; - - constructor( - private metadata: PromptRowWithId | PromptSessionEvent, - private defaults: DefaultPromptArgs, - private noTrace: boolean, - ) { - void this.__braintrust_prompt_marker; - } - - public get id(): HasId extends true ? string : string | undefined { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.metadata.id as HasId extends true ? string : string | undefined; - } - - public get projectId(): string | undefined { - return this.metadata.project_id; - } - - public get name(): string { - return "name" in this.metadata - ? this.metadata.name - : `Playground function ${this.metadata.id}`; - } - - public get slug(): string { - return "slug" in this.metadata ? this.metadata.slug : this.metadata.id; - } - - public get prompt(): PromptData["prompt"] { - return this.getParsedPromptData()?.prompt; - } - - public get version(): HasId extends true - ? TransactionId - : TransactionId | undefined { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.metadata[TRANSACTION_ID_FIELD] as HasId extends true - ? TransactionId - : TransactionId | undefined; - } - - public get options(): NonNullable { - return this.getParsedPromptData()?.options || {}; - } - - public get templateFormat(): string | null | undefined { - return this.getParsedPromptData()?.template_format; - } - - public get promptData(): PromptData { - return this.getParsedPromptData()!; - } - - /** - * Build the prompt with the given formatting options. The args you pass in will - * be forwarded to the mustache template that defines the prompt and rendered with - * the `mustache-js` library. - * - * @param buildArgs Args to forward along to the prompt template. - */ - public build( - buildArgs: unknown, - options: { - flavor?: Flavor; - messages?: Message[]; - strict?: boolean; - templateFormat?: TemplateFormat; - } = {}, - ): CompiledPrompt { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.runBuild(buildArgs, { - flavor: options.flavor ?? "chat", - messages: options.messages, - strict: options.strict, - templateFormat: options.templateFormat, - }) as CompiledPrompt; - } - - /** - * This is a special build method that first resolves attachment references, and then - * calls the regular build method. You should use this if you are building prompts from - * dataset rows that contain attachments. - * - * @param buildArgs Args to forward along to the prompt template. - */ - public async buildWithAttachments< - Flavor extends "chat" | "completion" = "chat", - >( - buildArgs: unknown, - options: { - flavor?: Flavor; - messages?: Message[]; - strict?: boolean; - state?: BraintrustState; - templateFormat?: TemplateFormat; - } = {}, - ): Promise> { - const hydrated = - buildArgs instanceof Object - ? await resolveAttachmentsToBase64(buildArgs, options.state) - : buildArgs; - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.runBuild(hydrated, { - flavor: options.flavor ?? "chat", - messages: options.messages, - strict: options.strict, - templateFormat: options.templateFormat, - }) as CompiledPrompt; - } - - private runBuild( - buildArgs: unknown, - options: { - flavor: Flavor; - messages?: Message[]; - strict?: boolean; - templateFormat?: TemplateFormat; - }, - ): CompiledPrompt { - const { flavor } = options; - - const params = Object.fromEntries( - Object.entries({ - ...this.defaults, - ...Object.fromEntries( - Object.entries(this.options.params || {}).filter( - ([k, _v]) => !BRAINTRUST_PARAMS.includes(k), - ), - ), - ...(!isEmpty(this.options.model) - ? { - model: this.options.model, - } - : {}), - }).filter(([key, value]) => key !== "response_format" || value !== null), - ); - - if (!("model" in params) || isEmpty(params.model)) { - throw new Error( - "No model specified. Either specify it in the prompt or as a default", - ); - } - - const spanInfo = this.noTrace - ? {} - : { - span_info: { - metadata: { - prompt: this.id - ? { - variables: buildArgs, - id: this.id, - project_id: this.projectId, - version: this.version, - ...("prompt_session_id" in this.metadata - ? { prompt_session_id: this.metadata.prompt_session_id } - : {}), - } - : undefined, - }, - }, - }; - - const prompt = this.prompt; - - if (!prompt) { - throw new Error("Empty prompt"); - } - - const dictArgParsed = z.record(z.unknown()).safeParse(buildArgs); - const variables: Record = { - input: buildArgs, - ...(dictArgParsed.success ? dictArgParsed.data : {}), - }; - - // Use template_format from prompt data if available, otherwise fall back to the option or default to mustache - const promptDataTemplateFormat = this.templateFormat; - const resolvedTemplateFormat = parseTemplateFormat( - options.templateFormat ?? promptDataTemplateFormat, - ); - - const renderedPrompt = Prompt.renderPrompt({ - prompt, - buildArgs, - options: { ...options, templateFormat: resolvedTemplateFormat }, - }); - - if (flavor === "chat") { - if (renderedPrompt.type !== "chat") { - throw new Error( - "Prompt is a completion prompt. Use buildCompletion() instead", - ); - } - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...renderPromptParams(params, variables, { - strict: options.strict, - templateFormat: resolvedTemplateFormat, - }), - ...spanInfo, - messages: renderedPrompt.messages, - ...(renderedPrompt.tools - ? { - tools: chatCompletionToolSchema - .array() - .parse(JSON.parse(renderedPrompt.tools)), - } - : undefined), - } as CompiledPrompt; - } else if (flavor === "completion") { - if (renderedPrompt.type !== "completion") { - throw new Error(`Prompt is a chat prompt. Use flavor: 'chat' instead`); - } - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...renderPromptParams(params, variables, { - strict: options.strict, - templateFormat: resolvedTemplateFormat, - }), - ...spanInfo, - prompt: renderedPrompt.content, - } as CompiledPrompt; - } else { - throw new Error("never!"); - } - } - - static renderPrompt({ - prompt, - buildArgs, - options, - }: { - prompt: PromptBlockData; - buildArgs: unknown; - options: { - strict?: boolean; - messages?: Message[]; - templateFormat?: TemplateFormat; - }; - }): PromptBlockData { - const escape = (v: unknown) => { - if (v === undefined) { - throw new Error("Missing!"); - } else if (typeof v === "string") { - return v; - } else if (v instanceof ReadonlyAttachment) { - throw new Error( - "Use buildWithAttachments() to build prompts with attachments", - ); - } else { - return JSON.stringify(v); - } - }; - - const dictArgParsed = z.record(z.unknown()).safeParse(buildArgs); - const variables: Record = { - input: buildArgs, - ...(dictArgParsed.success ? dictArgParsed.data : {}), - }; - - const templateFormat = parseTemplateFormat(options.templateFormat); - - if (prompt.type === "chat") { - const render = (template: string) => - renderTemplateContent(template, variables, escape, { - strict: options.strict, - templateFormat: templateFormat, - }); - - const baseMessages = (prompt.messages || []).map((m) => - renderMessageImpl(render, m, variables), - ); - const hasSystemPrompt = baseMessages.some((m) => m.role === "system"); - - const messages: Message[] = [ - ...baseMessages, - ...(options.messages ?? []).filter( - (m) => !(hasSystemPrompt && m.role === "system"), - ), - ]; - - return { - type: "chat", - messages: messages, - ...(prompt.tools?.trim() - ? { - tools: render(prompt.tools), - } - : undefined), - }; - } else if (prompt.type === "completion") { - if (options.messages) { - throw new Error( - "extra messages are not supported for completion prompts", - ); - } - - const content = renderTemplateContent(prompt.content, variables, escape, { - strict: options.strict, - templateFormat: templateFormat, - }); - return { - type: "completion", - content, - }; - } else { - const _: never = prompt; - throw new Error(`Invalid prompt type: ${_}`); - } - } - - private getParsedPromptData(): PromptData | undefined { - if (!this.hasParsedPromptData) { - this.parsedPromptData = promptDataSchema.parse(this.metadata.prompt_data); - this.hasParsedPromptData = true; - } - return this.parsedPromptData!; - } - - public static isPrompt(data: unknown): data is Prompt { - return ( - typeof data === "object" && - data !== null && - "__braintrust_prompt_marker" in data - ); - } - - static { - serializePromptForCache = (prompt) => ({ - metadata: prompt.metadata, - defaults: prompt.defaults, - noTrace: prompt.noTrace, - }); - } - - public static fromPromptData( - name: string, - promptData: PromptData, - ): Prompt { - return new Prompt( - { - name: name, - slug: name, - prompt_data: promptData, - }, - {}, - false, - ); - } -} - -export interface SerializedParameters { - metadata: ParametersRow; -} - -export let serializeParametersForCache: ( - parameters: RemoteEvalParameters, -) => SerializedParameters; - -export class RemoteEvalParameters< - HasId extends boolean = true, - HasVersion extends boolean = true, - T extends Record = Record, -> { - private readonly __braintrust_parameters_marker = true; - - constructor(private metadata: ParametersRow) { - void this.__braintrust_parameters_marker; - } - - public get id(): HasId extends true ? string : string | undefined { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.metadata.id as HasId extends true ? string : string | undefined; - } - - public get projectId(): string | undefined { - return this.metadata.project_id; - } - - public get name(): string { - return this.metadata.name; - } - - public get slug(): string { - return this.metadata.slug; - } - - public get version(): HasVersion extends true - ? TransactionId - : TransactionId | undefined { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.metadata[TRANSACTION_ID_FIELD] as HasVersion extends true - ? TransactionId - : TransactionId | undefined; - } - - public get schema(): Record { - return this.metadata.function_data.__schema; - } - - public get data(): T { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return (this.metadata.function_data.data ?? {}) as T; - } - - static { - serializeParametersForCache = (parameters) => ({ - metadata: parameters.metadata, - }); - } - - public validate(data: unknown): boolean { - if (typeof data !== "object" || data === null) { - return false; - } - const schemaProps = this.schema.properties; - if (typeof schemaProps !== "object" || schemaProps === null) { - return true; - } - for (const key of Object.keys(schemaProps)) { - if (!(key in data)) { - const required = Array.isArray(this.schema.required) - ? this.schema.required - : []; - if (required.includes(key)) { - return false; - } - } - } - return true; - } - - public static isParameters( - x: unknown, - ): x is RemoteEvalParameters> { - return ( - typeof x === "object" && - x !== null && - "__braintrust_parameters_marker" in x && - x.__braintrust_parameters_marker === true - ); - } -} - -export type AnyDataset = Dataset; - -/** - * Summary of a score's performance. - * @property name Name of the score. - * @property score Average score across all examples. - * @property diff Difference in score between the current and reference experiment. - * @property improvements Number of improvements in the score. - * @property regressions Number of regressions in the score. - */ -export interface ScoreSummary { - name: string; - score: number; - diff?: number; - improvements: number; - regressions: number; -} - -/** - * Summary of a metric's performance. - * @property name Name of the metric. - * @property metric Average metric across all examples. - * @property unit Unit label for the metric. - * @property diff Difference in metric between the current and reference experiment. - * @property improvements Number of improvements in the metric. - * @property regressions Number of regressions in the metric. - */ -export interface MetricSummary { - name: string; - metric: number; - unit: string; - diff?: number; - improvements: number; - regressions: number; -} - -/** - * Summary of an experiment's scores and metadata. - * @property projectName Name of the project that the experiment belongs to. - * @property experimentName Name of the experiment. - * @property experimentId ID of the experiment. May be `undefined` if the eval was run locally. - * @property projectUrl URL to the project's page in the Braintrust app. - * @property experimentUrl URL to the experiment's page in the Braintrust app. - * @property comparisonExperimentName The experiment scores are baselined against. - * @property scores Summary of the experiment's scores. - */ -export interface ExperimentSummary { - projectName: string; - experimentName: string; - projectId?: string; - experimentId?: string; - projectUrl?: string; - experimentUrl?: string; - comparisonExperimentName?: string; - scores: Record; - metrics?: Record; -} - -/** - * Summary of a dataset's data. - * - * @property totalRecords Total records in the dataset. - */ -export interface DataSummary { - /** - * New or updated records added in this session. - */ - newRecords: number; - /** - * Total records in the dataset. - */ - totalRecords: number; -} - -/** - * Summary of a dataset's scores and metadata. - * - * @property projectName Name of the project that the dataset belongs to. - * @property datasetName Name of the dataset. - * @property projectUrl URL to the project's page in the Braintrust app. - * @property datasetUrl URL to the experiment's page in the Braintrust app. - * @property dataSummary Summary of the dataset's data. - */ -export interface DatasetSummary { - projectName: string; - datasetName: string; - projectUrl: string; - datasetUrl: string; - dataSummary: DataSummary | undefined; -} - -/** - * Allows accessing helper functions for testing. - * @internal - */ - -// it's used to circumvent login for testing, but won't actually work -// on the server side. -const TEST_API_KEY = "___TEST_API_KEY__THIS_IS_NOT_REAL___"; - -function setInitialTestState() { - // a way of setting initial state for tests without any log messages - if (!_internalGetGlobalState()) { - _internalSetInitialState(); - } -} -// This is a helper function to simulate a login for testing. -async function simulateLoginForTests() { - return await login({ - apiKey: TEST_API_KEY, - appUrl: "https://braintrust.dev", - }); -} - -// This is a helper function to simulate a logout for testing. -function simulateLogoutForTests() { - const state = _internalGetGlobalState(); - state.resetLoginInfo(); - state.resetIdGenState(); - state[RESET_CONTEXT_MANAGER_STATE](); - state.appUrl = "https://www.braintrust.dev"; - return state; -} - -/** - * Get the versions for a prompt. - * - * @param projectId The ID of the project to query - * @param promptId The ID of the prompt to get versions for - * @returns Promise containing the version data - */ -export async function getPromptVersions( - projectId: string, - promptId: string, -): Promise { - const state = _internalGetGlobalState(); - if (!state) { - throw new Error("Must log in first"); - } - - await state.login({}); - - const query = { - from: { - op: "function", - name: { - op: "ident", - name: ["project_prompts"], - }, - args: [ - { - op: "literal", - value: projectId, - }, - ], - }, - select: [ - { - op: "star", - }, - ], - filter: { - op: "eq", - left: { op: "ident", name: ["id"] }, - right: { op: "literal", value: promptId }, - }, - }; - - const response = await state.apiConn().post( - "btql", - { - query, - audit_log: true, - use_columnstore: false, - brainstore_realtime: true, - }, - { headers: { "Accept-Encoding": "gzip" } }, - BTQL_HTTP_RETRIES, - ); - - if (!response.ok) { - throw new Error( - `API request failed: ${response.status} ${response.statusText}`, - ); - } - - const result = await response.json(); - - // Filter for entries where audit_data.action is "upsert" or "merge" and return only _xact_id fields - return ( - result.data - ?.filter((entry: any) => - ["upsert", "merge"].includes(entry.audit_data?.action), - ) - .map((entry: any) => prettifyXact(entry._xact_id)) || [] - ); -} - -// Helper function to reset ID generator state for testing -function resetIdGenStateForTests() { - const state = _internalGetGlobalState(); - if (state) { - state.resetIdGenState(); - } -} - -export const _exportsForTestingOnly = { - extractAttachments, +// Compatibility barrel for source consumers. Public entrypoints import feature modules directly. +export { + LOGS3_OVERFLOW_REFERENCE_TYPE, + DEFAULT_MAX_REQUEST_SIZE, + ContextParentSpanIds, + LoginInvalidOrgError, + SetCurrentArg, + StartSpanArgs, + EndSpanArgs, + Exportable, + Span, + CurrentSpanStore, + ContextManager, + getContextManager, + configureContextManager, + NoopSpan, + NOOP_SPAN, + NOOP_SPAN_PERMALINK, + SerializedBraintrustState, + BraintrustState, + CAPTURE_ATTACHMENTS, + _internalCaptureAttachmentsEnabled, + _internalSetInitialState, + _internalGetGlobalState, + registerOtelFlush, + FailedHTTPResponse, + ObjectMetadata, + AttachmentParams, + ExternalAttachmentParams, + BaseAttachment, + Attachment, + ExternalAttachment, + ReadonlyAttachment, + JSONAttachment, + updateSpan, + _internalResumeSpan, + PropagationContext, + spanComponentsToObjectId, + ERR_PERMALINK, + permalink, + Logger, + Logs3OverflowUpload, + logs3OverflowUploadSchema, + Logs3OverflowInputRow, + constructLogs3OverflowRequest, + pickLogs3OverflowObjectIds, + uploadLogs3OverflowPayload, + utf8ByteLength, + BackgroundLoggerOpts, + TestBackgroundLogger, + InitLoggerOptions, + initLogger, + LoginOptions, + FullLoginOptions, + setMaskingFunction, + login, + loginToState, + log, + summarize, + currentExperiment, + currentLogger, + currentSpan, + getSpanParentObject, + _internalExportParentSynchronously, + _injectIntoCarrier, + injectTraceContext, + extractTraceContextFromHeaders, + logError, + traced, + wrapTraced, + startSpan, + _internalStartSpan, + _internalStartSpanWithInitialMerge, + _internalStartSpanWithInitialMergeAndParentSpanIds, + _internalStartSpanWithContext, + flush, + setFetch, + withCurrent, + withParent, + _internalWithParent, deepCopyEvent, - useTestBackgroundLogger, - clearTestBackgroundLogger, - resetDebugLoggerForTests, - simulateLoginForTests, - simulateLogoutForTests, - setInitialTestState, - initTestExperiment, - isGeneratorFunction, - isAsyncGeneratorFunction, - resetIdGenStateForTests, - validateTags, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - isomorph: iso as any, // Expose isomorph for build type detection -}; + BaseMetadata, + DefaultMetadataType, + EvalCaseOrigin, + EvalCase, + newId, + SpanImpl, +} from "./logger/core"; +export { + DatasetRestorePreviewResult, + DatasetRestoreResult, + DatasetSnapshotLookup, + DatasetRef, + InitDatasetOptions, + FullInitDatasetOptions, + initDataset, + Dataset, + AnyDataset, + DataSummary, + DatasetSummary, +} from "./logger/datasets"; +export { + ParametersRef, + InitOptions, + FullInitOptions, + init, + Experiment, + ReadonlyExperiment, + ScoreSummary, + MetricSummary, + ExperimentSummary, +} from "./logger/experiments"; +export { + LoadPromptOptions, + LoadParametersByIdOptions, + LoadParametersByIdWithEnvOptions, + LoadParametersByProjectNameOptions, + LoadParametersByProjectNameWithEnvOptions, + LoadParametersByProjectIdOptions, + LoadParametersByProjectIdWithEnvOptions, + LoadParametersOptions, + loadPrompt, + loadParameters, + CompiledPromptParams, + ChatPrompt, + CompletionPrompt, + CompiledPrompt, + DefaultPromptArgs, + renderMessage, + renderMessageImpl, + PromptRowWithId, + deserializePlainStringAsJSON, + renderPromptParams, + SerializedPrompt, + serializePromptForCache, + Prompt, + SerializedParameters, + serializeParametersForCache, + RemoteEvalParameters, + getPromptVersions, +} from "./logger/prompts"; +export { + WithTransactionId, + DEFAULT_FETCH_BATCH_SIZE, + MAX_BTQL_ITERATIONS, + ObjectFetcher, +} from "./logger/objects"; +export { _exportsForTestingOnly } from "./logger/testing"; +export type { DatasetSnapshot } from "./sdk-types"; diff --git a/js/src/logger/core.ts b/js/src/logger/core.ts new file mode 100644 index 000000000..9b9c14abf --- /dev/null +++ b/js/src/logger/core.ts @@ -0,0 +1,6703 @@ +/// +import { v4 as uuidv4 } from "uuid"; +import { Queue, DEFAULT_QUEUE_SIZE } from "../queue"; +import { + debugLogger, + type DebugLogLevel, + type DebugLogLevelOption, + getEnvDebugLogLevel, + normalizeDebugLogLevelOption, + setDebugLogStateResolver, + setGlobalDebugLogLevel, +} from "../debug-logger"; +import { + IDGenerator, + getIdGenerator, + resolveUseLegacyUuidIds, +} from "../id-gen"; +import { + BAGGAGE_HEADER, + BRAINTRUST_PARENT_KEY, + PropagatedState, + TRACEPARENT_HEADER, + TraceContextCarrier, + TraceContextHeaders, + TRACESTATE_HEADER, + formatTraceparent, + getHeader, + mergeBaggage, + parseBaggage, + parseTraceparent, +} from "../propagation"; +import { + _urljoin, + AUDIT_METADATA_FIELD, + AUDIT_SOURCE_FIELD, + BackgroundLogEvent, + batchItems, + constructJsonArray, + ExperimentEvent, + ExperimentLogFullArgs, + ExperimentLogPartialArgs, + IdField, + IS_MERGE_FIELD, + LogFeedbackFullArgs, + mergeDicts, + mergeRowBatch, + OBJECT_DELETE_FIELD, + OBJECT_ID_KEYS, + SanitizedExperimentLogPartialArgs, + SpanComponentsV3, + SpanComponentsV4, + SpanComponentsV4Data, + SpanObjectTypeV3, + spanObjectTypeV3ToString, + SpanType, + SpanTypeAttribute, + TransactionId, + VALID_SOURCES, +} from "../util"; +import { + AttachmentReference as attachmentReferenceSchema, + AttachmentStatus as attachmentStatusSchema, + GitMetadataSettings as gitMetadataSettingsSchema, +} from "../sdk-schemas"; +import type { + AttachmentReference, + BraintrustAttachmentReference, + ExternalAttachmentReference, + AttachmentStatus, + GitMetadataSettings, + ObjectReference, +} from "../sdk-types"; +import { waitUntil } from "@vercel/functions"; +import { z, ZodError } from "zod/v3"; +import { + BraintrustStream, + createFinalValuePassThroughStream, + devNullWritableStream, +} from "../functions/stream"; +import iso, { IsoAsyncLocalStorage } from "#braintrust-platform"; +import { LRUCache } from "../lru-cache"; +import { + addAzureBlobHeaders, + getCurrentUnixTimestamp, + GLOBAL_PROJECT, + isEmpty, + LazyValue, + SyncLazyValue, + runCatchFinally, +} from "../util"; +import { SpanCache, CachedSpan } from "../span-cache"; +import { + detectSpanOriginEnvironment, + getSpanInstrumentationName, + INSTRUMENTATION_NAMES, + mergeSpanOriginContext, + type SpanOriginEnvironment, +} from "../span-origin"; +import { + createMaskingCustomizer, + customizeSpanExport, +} from "../span-customizer"; +import type { SpanCustomizer, SpanExportData } from "../instrumentation/config"; +import type { Experiment, ExperimentSummary } from "./experiments"; +const NODE_INSPECT_CUSTOM: unique symbol = Symbol.for( + "nodejs.util.inspect.custom", +); + +const TEST_API_KEY = "___TEST_API_KEY__THIS_IS_NOT_REAL___"; + +const BRAINTRUST_ATTACHMENT = "braintrust_attachment"; + +const EXTERNAL_ATTACHMENT = "external_attachment"; + +export const LOGS3_OVERFLOW_REFERENCE_TYPE = "logs3_overflow"; + +export const RESET_CONTEXT_MANAGER_STATE: unique symbol = Symbol.for( + "braintrust.resetContextManagerState", +); + +// 6 MB for the AWS lambda gateway (from our own testing). +export const DEFAULT_MAX_REQUEST_SIZE: number = 6 * 1024 * 1024; + +// Manual type definition for inline attachments (not in generated_types) +export const InlineAttachmentReferenceSchema: z.ZodType<{ + type: "inline_attachment"; + src: string; + content_type?: string; + filename?: string; +}> = z.object({ + type: z.literal("inline_attachment"), + src: z.string().min(1), + content_type: z.string().optional(), + filename: z.string().optional(), +}); + +// Context management interfaces +export interface ContextParentSpanIds { + rootSpanId: string; + spanParents: string[]; +} + +export class LoginInvalidOrgError extends Error { + constructor(public message: string) { + super(message); + } +} + +export type SetCurrentArg = { setCurrent?: boolean }; + +export type StartSpanEventArgs = ExperimentLogPartialArgs & Partial; + +const INITIAL_SPAN_WRITE_AS_MERGE: unique symbol = Symbol( + "braintrust.initial-span-write-as-merge", +); + +const RESUME_SPAN_WITHOUT_INITIAL_WRITE: unique symbol = Symbol( + "braintrust.resume-span-without-initial-write", +); + +const INTERNAL_SPAN_CONTEXT: unique symbol = Symbol( + "braintrust.internal-span-context", +); + +type InitialSpanWriteAsMergeArg = { + readonly [INITIAL_SPAN_WRITE_AS_MERGE]?: true; + readonly [RESUME_SPAN_WITHOUT_INITIAL_WRITE]?: true; +}; + +type InternalSpanContextArg = { + readonly [INTERNAL_SPAN_CONTEXT]?: Record; +}; + +export type StartSpanArgs = { + name?: string; + type?: SpanType; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + spanAttributes?: Record; + startTime?: number; + /** + * An opaque W3C trace context returned by + * {@link extractTraceContextFromHeaders}. + */ + parent?: PropagationContext; + event?: StartSpanEventArgs; + propagatedEvent?: StartSpanEventArgs; + spanId?: string; + parentSpanIds?: ParentSpanIds | MultiParentSpanIds; +}; + +type InternalStartSpanArgs = Omit & { + parent?: string | PropagationContext; +}; + +export type EndSpanArgs = { + endTime?: number; +}; + +export interface Exportable { + /** + * Return a serialized representation of the object that can be used to start subspans in other places. See {@link Span.traced} for more details. + */ + export(): Promise; +} + +/** + * A Span encapsulates logged data and metrics for a unit of work. This interface is shared by all span implementations. + * + * We suggest using one of the various `traced` methods, instead of creating Spans directly. See {@link Span.traced} for full details. + */ +export interface Span extends Exportable { + /** + * Row ID of the span. + */ + id: string; + + /** + * Span ID of the span. + */ + spanId: string; + + /** + * Root span ID of the span. + */ + rootSpanId: string; + + /** + * Parent span IDs of the span. + */ + spanParents: string[]; + + getParentInfo(): + | { + objectType: SpanObjectTypeV3; + objectId: LazyValue; + computeObjectMetadataArgs: Record | undefined; + } + | undefined; + + /** + * Incrementally update the current span with new data. The event will be batched and uploaded behind the scenes. + * + * @param event: Data to be logged. See {@link Experiment.log} for full details. + */ + log(event: ExperimentLogPartialArgs): void; + + /** + * Add feedback to the current span. Unlike `Experiment.logFeedback` and `Logger.logFeedback`, this method does not accept an id parameter, because it logs feedback to the current span. + * + * @param event: Data to be logged. See {@link Experiment.logFeedback} for full details. + */ + logFeedback(event: Omit): void; + + /** + * Create a new span and run the provided callback. This is useful if you want to log more detailed trace information beyond the scope of a single log event. Data logged over several calls to `Span.log` will be merged into one logical row. + * + * Spans created within `traced` are ended automatically. By default, the span is marked as current, so they can be accessed using `braintrust.currentSpan`. + * + * @param callback The function to be run under the span context. + * @param args.name Optional name of the span. If not provided, a name will be inferred from the call stack. + * @param args.type Optional type of the span. If not provided, the type will be unset. + * @param args.span_attributes Optional additional attributes to attach to the span, such as a type name. + * @param args.start_time Optional start time of the span, as a timestamp in seconds. + * @param args.setCurrent If true (the default), the span will be marked as the currently-active span for the duration of the callback. + * @param args.parent Optional parent info string for the span. The string can be generated from `[Span,Experiment,Logger].export`. If not provided, the current span will be used (depending on context). This is useful for adding spans to an existing trace. + * @param args.event Data to be logged. See {@link Experiment.log} for full details. + * @returns The result of running `callback`. + */ + traced( + callback: (span: Span) => R, + args?: StartSpanArgs & SetCurrentArg, + ): R; + + /** + * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations + * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", + * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead. + * + * See {@link Span.traced} for full details. + * + * @returns The newly-created `Span` + */ + startSpan(args?: StartSpanArgs): Span; + + /** + * Log an end time to the span (defaults to the current time). Returns the logged time. + * + * Will be invoked automatically if the span is constructed with `traced`. + * + * @param args.endTime Optional end time of the span, as a timestamp in seconds. + * @returns The end time logged to the span metrics. + */ + end(args?: EndSpanArgs): number; + + /** + * Serialize the identifiers of this span. The return value can be used to + * identify this span when starting a subspan elsewhere, such as another + * process or service, without needing to access this `Span` object. See the + * parameters of {@link Span.startSpan} for usage details. + * + * Callers should treat the return value as opaque. The serialization format + * may change from time to time. For cross-service propagation, prefer + * {@link Span.inject} and {@link extractTraceContextFromHeaders}. + * + * @returns Serialized representation of this span's identifiers. + */ + export(): Promise; + + /** + * Inject W3C trace-context headers (`traceparent` and `baggage`) for this span + * into a carrier, for distributed tracing across service boundaries. + * + * Adds `traceparent` (trace identity) and, when this span's Braintrust parent + * is known, a `baggage` entry `braintrust.parent=` (merged with any + * pre-existing baggage). Propagation is best-effort and never throws; if the + * span's ids are not W3C-shaped hex (e.g. legacy UUID mode), `traceparent` is + * omitted. + * + * @param carrier Optional existing carrier (e.g. outbound HTTP headers) to + * mutate and return. Supports plain objects, Web `Headers`, Node-style + * `setHeader` carriers, framework `header` carriers, and mutable + * `HeadersInit` tuple arrays. A new object is created if not provided. + * @returns The carrier with propagation headers injected. + */ + inject(): Record; + inject(carrier: T): T; + + /** + * Format a permalink to the Braintrust application for viewing this span. + * + * Links can be generated at any time, but they will only become viewable + * after the span and its root have been flushed to the server and ingested. + * + * This function can block resolving data with the server. For production + * applications it's preferable to call {@link Span.link} instead. + * + * @returns A promise which resolves to a permalink to the span. + */ + permalink(): Promise; + + /** + * Format a link to the Braintrust application for viewing this span. + * + * Links can be generated at any time, but they will only become viewable + * after the span and its root have been flushed to the server and ingested. + * + * There are some conditions when a Span doesn't have enough information + * to return a stable link (e.g. during an unresolved experiment). In this case + * or if there's an error generating link, we'll return a placeholder link. + * + * @returns A link to the span. + */ + link(): string; + + /** + * Flush any pending rows to the server. + */ + flush(): Promise; + + /** + * Set the span's name, type, or other attributes after it's created. + */ + setAttributes(args: Omit): void; + + /** + * Start a span with a specific id and parent span ids. + */ + startSpanWithParents( + spanId: string, + spanParents: string[], + args?: StartSpanArgs, + ): Span; + + /* + * Gets the span's `state` value, which is usually the global logging state (this is + * for very advanced purposes only) + */ + state(): BraintrustState; + + // For type identification. + kind: "span"; +} + +/** + * The AsyncLocalStorage contract used by context-manager integrations. + * + * The stored value is intentionally opaque (`unknown`) because the concrete type + * depends on the active context manager: + * - Default (`BraintrustContextManager`): stores a `Span` + * - OTEL compat (`OtelContextManager`): stores an OTEL `Context` object + * + * The global hook's `bindStore` transform (via `wrapSpanForStore`) produces the + * correct value type for whichever mode is active. + */ +export type CurrentSpanStore = IsoAsyncLocalStorage; + +export abstract class ContextManager { + abstract getParentSpanIds(): ContextParentSpanIds | undefined; + abstract runInContext(span: Span, callback: () => R): R; + abstract getCurrentSpan(): Span | undefined; + + /** + * Return the store used by instrumentation hooks to propagate the current + * span, when the context manager supports direct store binding. + */ + getCurrentSpanStore(): CurrentSpanStore | undefined { + return undefined; + } + + /** + * Returns the value to store in the ALS bound to a global hook's start event. + * In default mode this is the Span itself; in OTEL mode it is the OTEL Context + * containing the span so that OTEL's own ALS stores a proper Context object. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + wrapSpanForStore(span: Span): unknown { + return span; + } +} + +class BraintrustContextManager extends ContextManager { + private _currentSpan: IsoAsyncLocalStorage; + + constructor() { + super(); + this._currentSpan = iso.newAsyncLocalStorage(); + } + + getCurrentSpanStore(): CurrentSpanStore { + return this._currentSpan; + } + + getParentSpanIds(): ContextParentSpanIds | undefined { + const currentSpan = this._currentSpan.getStore(); + if (!currentSpan) { + return undefined; + } + + return { + rootSpanId: currentSpan.rootSpanId, + spanParents: [currentSpan.spanId], + }; + } + + runInContext(span: Span, callback: () => R): R { + return this._currentSpan.run(span, callback); + } + + getCurrentSpan(): Span | undefined { + return this._currentSpan.getStore(); + } +} + +declare global { + var BRAINTRUST_CONTEXT_MANAGER: (new () => ContextManager) | undefined; +} + +type SpanComponent = typeof SpanComponentsV3 | typeof SpanComponentsV4; + +// Return the active span-component exporter class. +// +// The export version is coupled to the active ID format: hex IDs (the default) +// serialize as V4, legacy UUID IDs serialize as V3. These must move together -- +// serializing hex IDs via V3 would lose the compact encoding and risk +// corrupting hex values that happen to parse as UUIDs. An explicit +export function getSpanComponentsClass(): SpanComponent { + return resolveUseLegacyUuidIds() ? SpanComponentsV3 : SpanComponentsV4; +} + +export function getContextManager(): ContextManager { + return globalThis.BRAINTRUST_CONTEXT_MANAGER + ? new globalThis.BRAINTRUST_CONTEXT_MANAGER() + : new BraintrustContextManager(); +} + +/** + * Configure the context manager used by Braintrust. + * + * This extension point is intended for runtime integrations such as + * `@braintrust/otel` and `@braintrust/browser`. Passing `undefined` restores + * Braintrust's default context manager. + */ +export function configureContextManager( + contextManager: (new () => ContextManager) | undefined, +): void { + globalThis.BRAINTRUST_CONTEXT_MANAGER = contextManager; + _globalState?.[RESET_CONTEXT_MANAGER_STATE](); +} + +/** + * A fake implementation of the Span API which does nothing. This can be used as the default span. + */ +export class NoopSpan implements Span { + public id: string; + public spanId: string; + public rootSpanId: string; + public spanParents: string[]; + + public kind: "span" = "span" as const; + + constructor() { + this.id = ""; + this.spanId = ""; + this.rootSpanId = ""; + this.spanParents = []; + } + + public log(_: ExperimentLogPartialArgs): void {} + + public logFeedback(_event: Omit): void {} + + public traced( + callback: (span: Span) => R, + _1?: StartSpanArgs & SetCurrentArg, + ): R { + return callback(this); + } + + public getParentInfo() { + return undefined; + } + + public startSpan(_1?: StartSpanArgs): this { + return this; + } + + public end(args?: EndSpanArgs): number { + return args?.endTime ?? getCurrentUnixTimestamp(); + } + + public async export(): Promise { + return ""; + } + + public inject(): Record; + public inject(carrier: T): T; + public inject( + carrier?: T, + ): T | Record { + return carrier ?? {}; + } + + public async permalink(): Promise { + return NOOP_SPAN_PERMALINK; + } + + public link(): string { + return NOOP_SPAN_PERMALINK; + } + + public async flush(): Promise {} + + public setAttributes(_args: Omit): void {} + + public startSpanWithParents( + _spanId: string, + _spanParents: string[], + _args?: StartSpanArgs, + ): Span { + return this; + } + + public state(): BraintrustState { + return _internalGetGlobalState(); + } + + // Custom inspect for Node.js console.log + static { + Object.defineProperty(this.prototype, NODE_INSPECT_CUSTOM, { + configurable: true, + writable: true, + value(this: NoopSpan): string { + return `NoopSpan { + kind: '${this.kind}', + id: '${this.id}', + spanId: '${this.spanId}', + rootSpanId: '${this.rootSpanId}', + spanParents: ${JSON.stringify(this.spanParents)} +}`; + }, + }); + } + + // Custom toString + toString(): string { + return `NoopSpan(id=${this.id}, spanId=${this.spanId})`; + } +} + +export const NOOP_SPAN: NoopSpan = new NoopSpan(); + +export const NOOP_SPAN_PERMALINK = "https://braintrust.dev/noop-span"; + +// In certain situations (e.g. the cli), we want separately-compiled modules to +// use the same state as the toplevel module. Use a symbol on `globalThis. +declare global { + interface Global { + [key: symbol]: any; + } +} + +const loginSchema = z.strictObject({ + appUrl: z.string(), + appPublicUrl: z.string(), + orgName: z.string(), + apiUrl: z.string(), + proxyUrl: z.string(), + loginToken: z.string(), + orgId: z.string().nullish(), + gitMetadataSettings: gitMetadataSettingsSchema.nullish(), + debugLogLevel: z.enum(["error", "warn", "info", "debug"]).optional(), + // Distinguishes explicit false from unset so env fallback stays disabled after deserialization. + debugLogLevelDisabled: z.boolean().optional(), +}); + +export type SerializedBraintrustState = { + appUrl: string; + appPublicUrl: string; + orgName: string; + apiUrl: string; + proxyUrl: string; + loginToken: string; + orgId?: string | null; + gitMetadataSettings?: GitMetadataSettings | null; + debugLogLevel?: "error" | "warn" | "info" | "debug"; + debugLogLevelDisabled?: boolean; +}; + +let stateNonce = 0; + +const V1_PROXY_SUFFIX = "/v1/proxy"; + +const LOADER_LOGIN_CACHE_MAX = 16; + +const LOGIN_TIMEOUT_MS = 30_000; + +const PROJECT_LOOKUP_TIMEOUT_MS = 30_000; + +const PROJECT_CACHE_TTL_MS = 15 * 60 * 1000; + +export type ResolvedLoaderLoginOptions = Required< + Pick +> & + Pick & { + forceLogin?: boolean; + credentialCacheNamespace: string; + existingState?: BraintrustState; + }; + +export type LoaderRequestState = Pick< + BraintrustState, + "appUrl" | "orgId" | "apiConn" +>; + +// proxyUrl may point at the universal proxy (`{apiUrl}/v1/proxy`) for +// EU/self-hosted orgs, but proxyConn only targets Braintrust API endpoints +// (e.g. function/invoke, function/sandbox-list) served at the API host root. +// Drop the suffix so these requests resolve on all data planes. +function normalizeProxyConnUrl(proxyUrl: string): string { + return proxyUrl.endsWith(V1_PROXY_SUFFIX) + ? proxyUrl.slice(0, proxyUrl.length - V1_PROXY_SUFFIX.length) + : proxyUrl; +} + +/** @internal */ +export const CAPTURE_ATTACHMENTS: unique symbol = Symbol.for( + "braintrust.captureAttachments", +); + +function captureAttachmentsFromEnv(): boolean { + return ["1", "true"].includes( + iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS")?.trim().toLowerCase() ?? "", + ); +} + +/** + * @internal Resolve automatic attachment capture for a logger or span. Only plain + * fields are read, so parents and global state from other SDK bundles also work. + */ +export function _internalCaptureAttachmentsEnabled(parent?: object): boolean { + // Global state is unset until a platform entrypoint configures the SDK. + const owner = parent ?? (_globalState ? getSpanParentObject() : undefined); + const policy = owner && Reflect.get(owner, CAPTURE_ATTACHMENTS); + return typeof policy === "boolean" + ? policy + : (_globalState?.captureAttachments ?? captureAttachmentsFromEnv()); +} + +export class BraintrustState { + public id: string; + public currentExperiment: Experiment | undefined; + public currentLogger: Logger | undefined; + public currentParent: IsoAsyncLocalStorage; + public currentSpan: IsoAsyncLocalStorage; + // Any time we re-log in, we directly update the apiConn inside the logger. + // This is preferable to replacing the whole logger, which would create the + // possibility of multiple loggers floating around, which may not log in a + // deterministic order. + private _bgLogger: SyncLazyValue; + private _overrideBgLogger: BackgroundLogger | null = null; + + public appUrl: string | null = null; + public appPublicUrl: string | null = null; + public loginToken: string | null = null; + public orgId: string | null = null; + public orgName: string | null = null; + public apiUrl: string | null = null; + public proxyUrl: string | null = null; + public loggedIn: boolean = false; + public gitMetadataSettings?: GitMetadataSettings; + /** @internal Default for instrumentation without a logger-specific policy. */ + public captureAttachments?: boolean; + public debugLogLevel?: DebugLogLevel; + private debugLogLevelConfigured = false; + + public fetch: typeof globalThis.fetch = globalThis.fetch; + private _appConn: HTTPConnection | null = null; + private _apiConn: HTTPConnection | null = null; + private _proxyConn: HTTPConnection | null = null; + private _spanCache: SpanCache | undefined; + + public get spanCache(): SpanCache { + return (this._spanCache ??= new SpanCache({ + disabled: this.loginParams.disableSpanCache, + })); + } + + public set spanCache(cache: SpanCache) { + this._spanCache = cache; + } + /** @internal */ + public readonly projectMetadataCache: LRUCache< + string, + { promise: Promise; expiresAt: number } + > = new LRUCache< + string, + { promise: Promise; expiresAt: number } + >({ max: 1000 }); + private _idGenerator: IDGenerator | null = null; + private _contextManager: ContextManager | null = null; + private _otelFlushCallback: (() => Promise) | null = null; + public spanOriginEnvironment: SpanOriginEnvironment | undefined; + private traceContextSigningSecret: string | undefined; + private loaderLoginCache = new WeakMap< + typeof globalThis.fetch, + LRUCache> + >(); + + private readonly loginParams: LoginOptions; + private activeLoginOrgNameSelector: string | undefined; + private loginGeneration = 0; + private loginQueueTail: Promise | undefined; + private pendingLogins = new WeakMap< + typeof globalThis.fetch, + Map> + >(); + + constructor(loginParams: LoginOptions) { + this.loginParams = { ...loginParams }; + this.activeLoginOrgNameSelector = + loginParams.orgName ?? iso.getEnv("BRAINTRUST_ORG_NAME"); + this.id = `${new Date().toLocaleString()}-${stateNonce++}`; // This is for debugging. uuidv4() breaks on platforms like Cloudflare. + this.currentExperiment = undefined; + this.currentLogger = undefined; + this.currentParent = iso.newAsyncLocalStorage< + string | PropagationContext + >(); + this.currentSpan = iso.newAsyncLocalStorage(); + + if (loginParams.fetch) { + this.fetch = loginParams.fetch; + } + + const defaultGetLogConn = async () => { + await this.login({}); + return this.apiConn(); + }; + this._bgLogger = new SyncLazyValue( + () => + new HTTPBackgroundLogger(new LazyValue(defaultGetLogConn), loginParams), + ); + + if (loginParams.debugLogLevel !== undefined) { + this.debugLogLevelConfigured = true; + this.debugLogLevel = normalizeDebugLogLevelOption( + loginParams.debugLogLevel, + ); + setGlobalDebugLogLevel(this.debugLogLevel ?? false); + } else { + this.debugLogLevel = getEnvDebugLogLevel(); + setGlobalDebugLogLevel(undefined); + } + + this.resetLoginInfo(); + + this.spanOriginEnvironment = detectSpanOriginEnvironment(); + this._internalSetTraceContextSigningSecret(loginParams.apiKey); + } + + /** @internal */ + public _internalSetTraceContextSigningSecret( + secret: string | undefined, + ): void { + const normalizedSecret = secret?.trim(); + if (normalizedSecret) { + this.traceContextSigningSecret = normalizedSecret; + } + } + + /** @internal */ + public _internalGetTraceContextSigningSecret(): string | undefined { + return ( + this.traceContextSigningSecret ?? + this.loginToken ?? + iso.getEnv("BRAINTRUST_API_KEY") + )?.trim(); + } + + public resetLoginInfo(): void { + this.loginGeneration++; + this.loginQueueTail = undefined; + this.pendingLogins = new WeakMap(); + this.projectMetadataCache.clear(); + this.appUrl = null; + this.appPublicUrl = null; + this.loginToken = null; + this.orgId = null; + this.orgName = null; + this.apiUrl = null; + this.proxyUrl = null; + this.loggedIn = false; + this.gitMetadataSettings = undefined; + + this._appConn = null; + this._apiConn = null; + this._proxyConn = null; + this.loaderLoginCache = new WeakMap(); + } + + /** @internal */ + public async _internalResolveLoaderLoginOptions({ + apiKey, + appUrl, + orgName, + fetch, + forceLogin, + }: Pick & { + forceLogin?: boolean; + }): Promise { + const resolvedAppUrl = + appUrl ?? + (this.loggedIn ? this.appUrl : undefined) ?? + this.loginParams.appUrl ?? + iso.getEnv("BRAINTRUST_APP_URL") ?? + "https://www.braintrust.dev"; + const resolvedApiKey = + apiKey ?? + (this.loggedIn ? this.loginToken : undefined) ?? + this.loginParams.apiKey ?? + (await iso.getBraintrustApiKey()); + if (!resolvedApiKey) { + throw new Error( + "Please specify an api key (e.g. by setting BRAINTRUST_API_KEY).", + ); + } + const normalizedApiKey = HTTPConnection.sanitize_token(resolvedApiKey); + const usesActiveCredential = + this.loggedIn && normalizedApiKey === this.loginToken; + const requestedOrgName = + orgName ?? + (usesActiveCredential ? this.activeLoginOrgNameSelector : undefined) ?? + this.loginParams.orgName ?? + iso.getEnv("BRAINTRUST_ORG_NAME"); + const resolvedOrgName = + orgName ?? + (usesActiveCredential ? this.orgName : undefined) ?? + requestedOrgName; + const resolvedFetch = + fetch ?? + (this.loggedIn ? this.fetch : undefined) ?? + this.loginParams.fetch ?? + globalThis.fetch; + const credentialCacheNamespace = JSON.stringify([ + "loader-credential", + resolvedAppUrl, + requestedOrgName, + normalizedApiKey, + ]); + + return { + apiKey: normalizedApiKey, + appUrl: resolvedAppUrl, + orgName: resolvedOrgName, + fetch: resolvedFetch, + forceLogin, + credentialCacheNamespace, + existingState: + !forceLogin && + usesActiveCredential && + resolvedAppUrl === this.appUrl && + resolvedOrgName === this.orgName && + resolvedFetch === this.fetch + ? this + : undefined, + }; + } + + /** @internal */ + public async _internalGetLoaderState({ + apiKey, + appUrl, + orgName, + fetch, + forceLogin, + existingState, + }: ResolvedLoaderLoginOptions): Promise { + if (existingState) { + return existingState; + } + + let cache = this.loaderLoginCache.get(fetch); + if (!cache) { + cache = new LRUCache({ max: LOADER_LOGIN_CACHE_MAX }); + this.loaderLoginCache.set(fetch, cache); + } + + const cacheKey = JSON.stringify([appUrl, orgName, apiKey]); + if (!forceLogin) { + const cachedState = cache.get(cacheKey); + if (cachedState) { + return cachedState; + } + } + + const statePromise = loginToLoaderRequestState({ + orgName, + apiKey, + appUrl, + fetch, + }); + cache.set(cacheKey, statePromise); + + try { + return await statePromise; + } catch (error) { + if (cache.get(cacheKey) === statePromise) { + cache.delete(cacheKey); + } + throw error; + } + } + + public resetIdGenState(): void { + // Reset the ID generator so it gets recreated with current environment variables + this._idGenerator = null; + } + + static { + Object.defineProperty(this.prototype, RESET_CONTEXT_MANAGER_STATE, { + configurable: true, + writable: true, + value(this: BraintrustState): void { + this._contextManager = null; + }, + }); + } + + public get idGenerator(): IDGenerator { + if (this._idGenerator === null) { + this._idGenerator = getIdGenerator(); + } + return this._idGenerator; + } + + public get contextManager(): ContextManager { + if (this._contextManager === null) { + this._contextManager = getContextManager(); + } + return this._contextManager; + } + + /** + * Register an OTEL flush callback. This is called by @braintrust/otel + * when it initializes a BraintrustSpanProcessor/Exporter. + */ + public registerOtelFlush(callback: () => Promise): void { + this._otelFlushCallback = callback; + } + + /** + * Flush OTEL spans if a callback is registered. + * Called during ensureSpansFlushed to ensure OTEL spans are visible in BTQL. + */ + public async flushOtel(): Promise { + if (this._otelFlushCallback) { + await this._otelFlushCallback(); + } + } + + public copyLoginInfo(other: BraintrustState): void { + this.projectMetadataCache.clear(); + this.appUrl = other.appUrl; + this.appPublicUrl = other.appPublicUrl; + this.loginToken = other.loginToken; + this.orgId = other.orgId; + this.orgName = other.orgName; + this.apiUrl = other.apiUrl; + this.proxyUrl = other.proxyUrl; + this.loggedIn = other.loggedIn; + this.gitMetadataSettings = other.gitMetadataSettings; + this.debugLogLevel = other.debugLogLevel; + this.debugLogLevelConfigured = other.debugLogLevelConfigured; + this.traceContextSigningSecret = other.traceContextSigningSecret; + this.fetch = other.fetch; + this.activeLoginOrgNameSelector = other.activeLoginOrgNameSelector; + setGlobalDebugLogLevel( + this.debugLogLevelConfigured ? (this.debugLogLevel ?? false) : undefined, + ); + + this._appConn = other._appConn; + this._apiConn = other._apiConn; + this.loginReplaceApiConn(this.apiConn()); + this._proxyConn = other._proxyConn; + } + + public serialize(): SerializedBraintrustState { + if (!this.loggedIn) { + throw new Error( + "Cannot serialize BraintrustState without being logged in", + ); + } + + if ( + !this.appUrl || + !this.appPublicUrl || + !this.apiUrl || + !this.proxyUrl || + !this.orgName || + !this.loginToken || + !this.loggedIn + ) { + throw new Error( + "Cannot serialize BraintrustState without all login attributes", + ); + } + + return { + appUrl: this.appUrl, + appPublicUrl: this.appPublicUrl, + loginToken: this.loginToken, + orgId: this.orgId, + orgName: this.orgName, + apiUrl: this.apiUrl, + proxyUrl: this.proxyUrl, + gitMetadataSettings: this.gitMetadataSettings, + ...(this.debugLogLevel ? { debugLogLevel: this.debugLogLevel } : {}), + ...(this.debugLogLevelConfigured && !this.debugLogLevel + ? { debugLogLevelDisabled: true } + : {}), + }; + } + + static deserialize( + serialized: unknown, + opts?: LoginOptions, + ): BraintrustState { + const serializedParsed = loginSchema.safeParse(serialized); + if (!serializedParsed.success) { + throw new Error( + `Cannot deserialize BraintrustState: ${serializedParsed.error.message}`, + ); + } + const state = new BraintrustState({ ...opts }); + for (const key of Object.keys(loginSchema.shape)) { + (state as any)[key] = (serializedParsed.data as any)[key]; + } + + if (!state.loginToken) { + throw new Error( + "Cannot deserialize BraintrustState without a login token", + ); + } + + state.apiConn().set_token(state.loginToken); + state.apiConn().make_long_lived(); + state.appConn().set_token(state.loginToken); + if (state.proxyUrl) { + state.proxyConn().make_long_lived(); + state.proxyConn().set_token(state.loginToken); + } + + state.loggedIn = true; + state.debugLogLevelConfigured = + "debugLogLevel" in serializedParsed.data || + !!serializedParsed.data.debugLogLevelDisabled; + setGlobalDebugLogLevel( + state.debugLogLevelConfigured + ? (state.debugLogLevel ?? false) + : undefined, + ); + state.loginReplaceApiConn(state.apiConn()); + + return state; + } + + public setFetch(fetch: typeof globalThis.fetch): void { + if (fetch !== this.fetch) { + this.projectMetadataCache.clear(); + } + this.loginParams.fetch = fetch; + this.fetch = fetch; + this._apiConn?.setFetch(fetch); + this._appConn?.setFetch(fetch); + } + + public setMaskingFunction( + maskingFunction: ((value: unknown) => unknown) | null, + ): void { + this.bgLogger().setMaskingFunction(maskingFunction); + } + + public setDebugLogLevel(option: DebugLogLevelOption): void { + if (option === undefined) { + return; + } + this.debugLogLevelConfigured = true; + this.debugLogLevel = normalizeDebugLogLevelOption(option); + setGlobalDebugLogLevel(this.debugLogLevel ?? false); + } + + public getDebugLogLevel(): DebugLogLevel | undefined { + return this.debugLogLevel; + } + + public hasDebugLogLevelOverride(): boolean { + return this.debugLogLevelConfigured; + } + + public async login( + loginParams: LoginOptions & { forceLogin?: boolean }, + ): Promise { + this._internalSetTraceContextSigningSecret(loginParams.apiKey); + this.setDebugLogLevel(loginParams.debugLogLevel); + if (this.apiUrl && !loginParams.forceLogin) { + return; + } + const generation = this.loginGeneration; + const pendingLogins = this.pendingLogins; + const options = { + ...this.loginParams, + ...Object.fromEntries( + Object.entries(loginParams).filter(([k, v]) => !isEmpty(v)), + ), + }; + const appUrl = + options.appUrl ?? + (iso.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev"); + const orgName = options.orgName ?? iso.getEnv("BRAINTRUST_ORG_NAME"); + const fetch = options.fetch ?? globalThis.fetch; + const apiKey = options.apiKey ?? (await iso.getBraintrustApiKey()); + if (generation !== this.loginGeneration) { + throw new Error("Login cancelled by a reset."); + } + // Another attempt may have finished while the credential was being read. + if (this.apiUrl && !loginParams.forceLogin) { + if ( + this.appUrl !== appUrl || + this.loginToken !== (apiKey && HTTPConnection.sanitize_token(apiKey)) || + this.activeLoginOrgNameSelector !== orgName || + this.fetch !== fetch + ) { + throw new Error( + "Another login completed with different options during credential discovery. To force re-login, pass `forceLogin: true`.", + ); + } + return; + } + + let pending = pendingLogins.get(fetch); + if (!pending) { + pending = new Map(); + pendingLogins.set(fetch, pending); + } + const key = JSON.stringify([ + appUrl, + orgName, + apiKey, + options.debugLogLevel, + ]); + const existing = pending.get(key); + if (!loginParams.forceLogin && existing) { + return existing; + } + + const previousLogin = this.loginQueueTail; + const loginPromise = (async () => { + // Each forced login still makes a fresh request, but must not invalidate + // the metadata promises of loggers already waiting for another login. + if (previousLogin) { + await previousLogin.catch(() => {}); + } + if (generation !== this.loginGeneration) { + throw new Error("Login cancelled by a reset."); + } + const newState = await loginToState({ + ...options, + appUrl, + orgName, + apiKey, + fetch, + }); + if (generation !== this.loginGeneration) { + throw new Error("Login cancelled by a reset."); + } + this.copyLoginInfo(newState); + })(); + this.loginQueueTail = loginPromise; + pending.set(key, loginPromise); + try { + await loginPromise; + } finally { + if (this.loginQueueTail === loginPromise) { + this.loginQueueTail = undefined; + } + if (pending.get(key) === loginPromise) { + pending.delete(key); + } + } + } + + public appConn(): HTTPConnection { + if (!this._appConn) { + if (!this.appUrl) { + throw new Error("Must initialize appUrl before requesting appConn"); + } + this._appConn = new HTTPConnection(this.appUrl, this.fetch); + } + return this._appConn!; + } + + public apiConn(): HTTPConnection { + if (!this._apiConn) { + if (!this.apiUrl) { + throw new Error("Must initialize apiUrl before requesting apiConn"); + } + this._apiConn = new HTTPConnection(this.apiUrl, this.fetch); + } + return this._apiConn!; + } + + public proxyConn(): HTTPConnection { + if (!this.proxyUrl) { + return this.apiConn(); + } + if (!this._proxyConn) { + if (!this.proxyUrl) { + throw new Error("Must initialize proxyUrl before requesting proxyConn"); + } + this._proxyConn = new HTTPConnection( + normalizeProxyConnUrl(this.proxyUrl), + this.fetch, + ); + } + return this._proxyConn!; + } + + public bgLogger(): BackgroundLogger { + if (this._overrideBgLogger) { + return this._overrideBgLogger; + } + return this._bgLogger.get(); + } + + public httpLogger(): HTTPBackgroundLogger { + // this is called for configuration in some end-to-end tests so + // expose the http bg logger here. + return this._bgLogger.get(); + } + + public setOverrideBgLogger(logger: BackgroundLogger | null): void { + this._overrideBgLogger = logger; + } + + // Should only be called by the login function. + public loginReplaceApiConn(apiConn: HTTPConnection): void { + this._bgLogger.get().internalReplaceApiConn(apiConn); + } + + public disable(): void { + this._bgLogger.get().disable(); + } + + public enforceQueueSizeLimit(enforce: boolean): void { + this._bgLogger.get().enforceQueueSizeLimit(enforce); + } + + // Custom serialization to avoid logging sensitive data + toJSON(): Record { + return { + id: this.id, + orgId: this.orgId, + orgName: this.orgName, + appUrl: this.appUrl, + appPublicUrl: this.appPublicUrl, + apiUrl: this.apiUrl, + proxyUrl: this.proxyUrl, + loggedIn: this.loggedIn, + // Explicitly exclude loginToken, _apiConn, _appConn, _proxyConn and other sensitive fields + }; + } + + // Custom inspect for Node.js console.log + static { + Object.defineProperty(this.prototype, NODE_INSPECT_CUSTOM, { + configurable: true, + writable: true, + value(this: BraintrustState): string { + return `BraintrustState { + id: '${this.id}', + orgId: ${this.orgId ? `'${this.orgId}'` : "null"}, + orgName: ${this.orgName ? `'${this.orgName}'` : "null"}, + appUrl: ${this.appUrl ? `'${this.appUrl}'` : "null"}, + apiUrl: ${this.apiUrl ? `'${this.apiUrl}'` : "null"}, + proxyUrl: ${this.proxyUrl ? `'${this.proxyUrl}'` : "null"}, + loggedIn: ${this.loggedIn}, + loginToken: '[REDACTED]' +}`; + }, + }); + } + + // Custom toString + toString(): string { + return `BraintrustState(id=${this.id}, org=${this.orgName || "none"}, loggedIn=${this.loggedIn})`; + } +} + +let _globalState: BraintrustState; + +/** + * Initialize the global Braintrust state lazily, it only creates the BraintrustState instance on the first invocation. + * + * The state is stored in a global symbol to ensure cross-bundle compatibility. + * + * This is invoked on first use; importing the SDK leaves global state untouched. + * + * @internal + */ +export function _internalSetInitialState(): void { + if (_globalState) { + return; + } + const sym = Symbol.for("braintrust-state"); + let existing = (globalThis as any)[sym]; + if (!existing) { + const state = new BraintrustState({}); + (globalThis as any)[sym] = state; + existing = state; + } + _globalState = existing; + setDebugLogStateResolver(() => _globalState); +} + +/** + * @internal + */ +export function _internalGetGlobalState(): BraintrustState { + _internalSetInitialState(); + return _globalState; +} + +/** + * Register a callback to flush OTEL spans. This is called by @braintrust/otel + * when it initializes a BraintrustSpanProcessor/Exporter. + * + * When ensureSpansFlushed is called (e.g., before a BTQL query in scorers), + * this callback will be invoked to ensure OTEL spans are flushed to the server. + * + * Also disables the span cache, since OTEL spans aren't in the local cache + * and we need BTQL to see the complete span tree (both native + OTEL spans). + */ +export function registerOtelFlush(callback: () => Promise): void { + _internalGetGlobalState().registerOtelFlush(callback); + // Disable span cache since OTEL spans aren't in the local cache + _internalGetGlobalState().spanCache.disable(); +} + +export class FailedHTTPResponse extends Error { + public status: number; + public text: string; + public data: string; + public readonly cause?: unknown; + + constructor(status: number, text: string, data: string, cause?: unknown) { + super(`${status}: ${text} (${data})`); + this.status = status; + this.text = text; + this.data = data; + this.cause = cause; + } +} + +class HTTPTransportError extends Error { + public readonly cause: unknown; + + constructor(cause: unknown) { + super(cause instanceof Error ? cause.message : String(cause)); + this.name = "HTTPTransportError"; + this.cause = cause; + } +} + +const httpTransportErrorCauses = new WeakSet(); + +function recordHTTPTransportError(error: unknown): void { + if ( + (typeof error === "object" && error !== null) || + typeof error === "function" + ) { + httpTransportErrorCauses.add(error); + } +} + +function rethrowHTTPTransportError( + error: unknown, + classifyTransportErrors: boolean, +): never { + if (classifyTransportErrors) { + throw new HTTPTransportError(error); + } + recordHTTPTransportError(error); + throw error; +} + +export function isLoaderCacheFallbackError(error: unknown): boolean { + if (error instanceof FailedHTTPResponse) { + return error.status === 408 || error.status === 429 || error.status >= 500; + } + + if (error instanceof HTTPTransportError) { + return true; + } + + return ( + ((typeof error === "object" && error !== null) || + typeof error === "function") && + httpTransportErrorCauses.has(error) + ); +} + +async function readJSONResponse( + response: Response, + classifyTransportErrors = false, +) { + let data: string; + try { + data = await response.text(); + } catch (error) { + rethrowHTTPTransportError(error, classifyTransportErrors); + } + return JSON.parse(data); +} + +async function checkResponse(resp: Response) { + if (resp.ok) { + return resp; + } + + let data: string; + try { + data = await resp.text(); + } catch (error) { + throw new FailedHTTPResponse( + resp.status, + resp.statusText, + "Unable to read response body", + error, + ); + } + throw new FailedHTTPResponse(resp.status, resp.statusText, data); +} + +export class HTTPConnection { + base_url: string; + token: string | null; + headers: Record; + fetch: typeof globalThis.fetch; + + constructor( + base_url: string, + fetch: typeof globalThis.fetch, + private readonly classifyTransportErrors = false, + ) { + this.base_url = base_url; + this.token = null; + this.headers = {}; + + this._reset(); + this.fetch = fetch; + } + + public setFetch(fetch: typeof globalThis.fetch): void { + this.fetch = fetch; + } + + async ping(): Promise { + try { + const resp = await this.get("ping"); + return resp.status === 200; + } catch { + return false; + } + } + + make_long_lived(): void { + // Following a suggestion in https://stackoverflow.com/questions/23013220/max-retries-exceeded-with-url-in-requests + this._reset(); + } + + static sanitize_token(token: string): string { + return token.trim(); + } + + set_token(token: string): void { + token = HTTPConnection.sanitize_token(token); + this.token = token; + this._reset(); + } + + // As far as I can tell, you cannot set the retry/backoff factor here + _reset(): void { + this.headers = {}; + if (this.token) { + this.headers["Authorization"] = `Bearer ${this.token}`; + } + } + + async get( + path: string, + params: + | Record + | undefined = undefined, + config?: RequestInit, + ): Promise { + const { headers, ...rest } = config || {}; + const url = new URL(_urljoin(this.base_url, path)); + url.search = new URLSearchParams( + params + ? Object.entries(params) + .filter(([_, v]) => v !== undefined) + .flatMap(([k, v]) => + v !== undefined + ? typeof v === "string" + ? [[k, v]] + : v.map((x) => [k, x]) + : [], + ) + : [], + ).toString(); + + // On platforms like Cloudflare, we lose "this" when we make an async call, + // so we need to bind it again. + const this_fetch = this.fetch; + const this_headers = this.headers; + let response: Response; + try { + // Using toString() here makes it work with isomorphic fetch + response = await this_fetch(url.toString(), { + headers: { + Accept: "application/json", + ...this_headers, + ...headers, + }, + keepalive: true, + ...rest, + }); + } catch (error) { + if (config?.signal?.aborted) { + throw getAbortReason(config.signal); + } + rethrowHTTPTransportError(error, this.classifyTransportErrors); + } + return await checkResponse(response); + } + + async post( + path: string, + params?: Record | string, + config?: RequestInit, + retries: number = 0, + ): Promise { + const { headers, ...rest } = config || {}; + // On platforms like Cloudflare, we lose "this" when we make an async call, + // so we need to bind it again. + const this_fetch = this.fetch; + const this_base_url = this.base_url; + const this_headers = this.headers; + + const tries = retries + 1; + for (let i = 0; i < tries; i++) { + try { + let response: Response; + try { + response = await this_fetch(_urljoin(this_base_url, path), { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + ...this_headers, + ...headers, + }, + body: + typeof params === "string" + ? params + : params + ? JSON.stringify(params) + : undefined, + keepalive: true, + ...rest, + }); + } catch (error) { + if (config?.signal?.aborted) { + throw getAbortReason(config.signal); + } + rethrowHTTPTransportError(error, this.classifyTransportErrors); + } + return await checkResponse(response); + } catch (error) { + if (config?.signal?.aborted) { + throw getAbortReason(config.signal); + } + if (i === tries - 1 || !isRetryableHTTPError(error)) { + throw error; + } + + debugLogger.debug( + `Retrying API request ${path} after ${formatHTTPError(error)}`, + ); + const sleepTimeMs = + HTTP_RETRY_BASE_SLEEP_TIME_S * 1000 * 2 ** i + + Math.random() * HTTP_RETRY_JITTER_MS; + debugLogger.info( + `Sleeping for ${sleepTimeMs}ms before retrying API request`, + ); + await waitForRetry(sleepTimeMs, config?.signal); + } + } + + throw new Error("Unexpected retry state"); + } + + async get_json( + object_type: string, + args: Record | undefined = undefined, + retries: number = 0, + signal?: AbortSignal, + ): Promise { + const tries = retries + 1; + for (let i = 0; i < tries; i++) { + try { + const resp = await this.get(`${object_type}`, args, { signal }); + return await readJSONResponse(resp, this.classifyTransportErrors); + } catch (e) { + if (signal?.aborted) { + throw getAbortReason(signal); + } + if (i < tries - 1) { + debugLogger.debug( + `Retrying API request ${object_type} ${JSON.stringify(args)} ${ + (e as any).status + } ${(e as any).text}`, + ); + const sleepTimeS = HTTP_RETRY_BASE_SLEEP_TIME_S * 2 ** i; + debugLogger.info( + `Sleeping for ${sleepTimeS}s before retrying API request`, + ); + await waitForRetry(sleepTimeS * 1000, signal); + continue; + } + throw e; + } + } + } + + async post_json( + object_type: string, + args: Record | string | undefined = undefined, + signal?: AbortSignal, + ): Promise { + const resp = await this.post(`${object_type}`, args, { + headers: { "Content-Type": "application/json" }, + signal, + }); + return await readJSONResponse(resp, this.classifyTransportErrors); + } + + // Custom inspect for Node.js console.log + static { + Object.defineProperty(this.prototype, NODE_INSPECT_CUSTOM, { + configurable: true, + writable: true, + value(this: HTTPConnection): string { + return `HTTPConnection { + base_url: '${this.base_url}', + token: '[REDACTED]' +}`; + }, + }); + } + + // Custom toString + toString(): string { + return `HTTPConnection(${this.base_url})`; + } +} + +export interface ObjectMetadata { + id: string; + name: string; + fullInfo: Record; +} + +export interface ProjectExperimentMetadata { + project: ObjectMetadata; + experiment: ObjectMetadata; +} + +export interface ProjectDatasetMetadata { + project: ObjectMetadata; + dataset: ObjectMetadata; +} + +interface OrgProjectMetadata { + org_id: string; + project: ObjectMetadata; +} + +interface LinkArgs { + org_name?: string; + app_url?: string; +} + +type ProjectMetadataArgs = { + project_group_name?: string; + project_name?: string; + project_id?: string; +}; + +export interface AttachmentParams { + data: string | Blob | ArrayBuffer; + filename: string; + contentType: string; + state?: BraintrustState; +} + +export interface ExternalAttachmentParams { + url: string; + filename: string; + contentType: string; + state?: BraintrustState; +} + +export abstract class BaseAttachment { + readonly reference!: AttachmentReference; + abstract upload(): Promise; + abstract data(): Promise; + abstract debugInfo(): Record; +} + +type DatasetPipelineDeferredJSONAttachmentHook = ( + data: unknown, + options?: { filename?: string; pretty?: boolean }, +) => object; + +declare global { + // Set by the bt dataset pipeline runner so JSONAttachment can be represented + // as a destination-uploaded marker during transform. + // eslint-disable-next-line no-var + var __BT_DATASET_PIPELINE_DEFER_JSON_ATTACHMENT__: + | DatasetPipelineDeferredJSONAttachmentHook + | undefined; +} + +/** + * Represents an attachment to be uploaded and the associated metadata. + * `Attachment` objects can be inserted anywhere in an event, allowing you to + * log arbitrary file data. The SDK will asynchronously upload the file to + * object storage and replace the `Attachment` object with an + * `AttachmentReference`. + */ +export class Attachment extends BaseAttachment { + /** + * The object that replaces this `Attachment` at upload time. + */ + readonly reference: BraintrustAttachmentReference; + + private readonly uploader: LazyValue; + private readonly _data: LazyValue; + private readonly state?: BraintrustState; + // For debug logging only. + private readonly dataDebugString: string; + + /** + * Construct an attachment. + * + * @param param A parameter object with: + * + * `data`: A string representing the path of the file on disk, or a + * `Blob`/`ArrayBuffer` with the file's contents. The caller is responsible + * for ensuring the file/blob/buffer is not modified until upload is complete. + * + * `filename`: The desired name of the file in Braintrust after uploading. + * This parameter is for visualization purposes only and has no effect on + * attachment storage. + * + * `contentType`: The MIME type of the file. + * + * `state`: (Optional) For internal use. + */ + constructor({ data, filename, contentType, state }: AttachmentParams) { + super(); + this.reference = { + type: BRAINTRUST_ATTACHMENT, + filename, + content_type: contentType, + key: newId(), + }; + this.state = state; + this.dataDebugString = typeof data === "string" ? data : ""; + + this._data = this.initData(data); + this.uploader = this.initUploader(); + } + + /** + * On first access, (1) reads the attachment from disk if needed, (2) + * authenticates with the data plane to request a signed URL, (3) uploads to + * object store, and (4) updates the attachment. + * + * @returns The attachment status. + */ + async upload(): Promise { + return await this.uploader.get(); + } + + /** + * The attachment contents. This is a lazy value that will read the attachment contents from disk or memory on first access. + */ + async data(): Promise { + return this._data.get(); + } + + /** + * A human-readable description for logging and debugging. + * + * @returns The debug object. The return type is not stable and may change in + * a future release. + */ + debugInfo(): Record { + return { + inputData: this.dataDebugString, + reference: this.reference, + state: this.state, + }; + } + + private initUploader(): LazyValue { + const doUpload = async (conn: HTTPConnection, orgId: string) => { + const requestParams = { + key: this.reference.key, + filename: this.reference.filename, + content_type: this.reference.content_type, + org_id: orgId, + }; + const [metadataPromiseResult, dataPromiseResult] = + await Promise.allSettled([ + conn.post("/attachment", requestParams), + this._data.get(), + ]); + if (metadataPromiseResult.status === "rejected") { + const errorStr = JSON.stringify(metadataPromiseResult.reason); + throw new Error( + `Failed to request signed URL from API server: ${errorStr}`, + ); + } + if (dataPromiseResult.status === "rejected") { + const errorStr = JSON.stringify(dataPromiseResult.reason); + throw new Error(`Failed to read file: ${errorStr}`); + } + const metadataResponse = metadataPromiseResult.value; + const data = dataPromiseResult.value; + + let signedUrl: string | undefined; + let headers: Record; + try { + ({ signedUrl, headers } = z + .object({ + signedUrl: z.string().url(), + headers: z.record(z.string()), + }) + .parse(await metadataResponse.json())); + } catch (error) { + if (error instanceof ZodError) { + const errorStr = JSON.stringify(error.flatten()); + throw new Error(`Invalid response from API server: ${errorStr}`); + } + throw error; + } + + addAzureBlobHeaders(headers, signedUrl); + + // TODO multipart upload. + let objectStoreResponse: Response | undefined; + try { + objectStoreResponse = await checkResponse( + await fetch(signedUrl, { + method: "PUT" satisfies RequestInit["method"], + headers, + body: data, + }), + ); + await objectStoreResponse.body?.cancel(); + } catch (error) { + if (error instanceof FailedHTTPResponse) { + throw new Error( + `Failed to upload attachment to object store: ${error.status} ${error.text} ${error.data}`, + ); + } + throw error; + } + + return { signedUrl, metadataResponse, objectStoreResponse }; + }; + + // Catches error messages and updates the attachment status. + const errorWrapper = async () => { + const status: AttachmentStatus = { upload_status: "done" }; + + const state = this.state ?? _internalGetGlobalState(); + await state.login({}); + + const conn = state.apiConn(); + const orgId = state.orgId ?? ""; + + try { + await doUpload(conn, orgId); + } catch (error) { + status.upload_status = "error"; + status.error_message = + error instanceof Error ? error.message : JSON.stringify(error); + } + + const requestParams = { + key: this.reference.key, + org_id: orgId, + status, + }; + const statusResponse = await conn.post( + "/attachment/status", + requestParams, + ); + await statusResponse.body?.cancel(); + if (!statusResponse.ok) { + const errorStr = JSON.stringify(statusResponse); + throw new Error(`Couldn't log attachment status: ${errorStr}`); + } + + return status; + }; + + return new LazyValue(errorWrapper); + } + + private initData(data: string | Blob | ArrayBuffer): LazyValue { + if (typeof data === "string") { + this.ensureFileReadable(data); + + const readFile = iso.readFile; + if (!readFile) { + throw new Error( + `This platform does not support reading the filesystem. Construct the Attachment +with a Blob/ArrayBuffer, or run the program on Node.js.`, + ); + } + // This could stream the file in the future. + return new LazyValue( + async () => new Blob([Uint8Array.from(await readFile(data))]), + ); + } else { + return new LazyValue(async () => new Blob([data])); + } + } + + private ensureFileReadable(data: string) { + const statSync = iso.statSync; + if (!statSync) { + throw new Error( + `This platform does not support reading the filesystem. Construct the Attachment +with a Blob/ArrayBuffer, or run the program on Node.js.`, + ); + } + + try { + statSync(data); + } catch (e) { + debugLogger.warn(`Failed to read file: ${e}`); + } + } +} + +/** + * Represents an attachment that resides in an external object store and the associated metadata. + * + * `ExternalAttachment` objects can be inserted anywhere in an event, similar to + * `Attachment` objects, but they reference files that already exist in an external + * object store rather than requiring upload. The SDK will replace the `ExternalAttachment` + * object with an `AttachmentReference` during logging. + */ +export class ExternalAttachment extends BaseAttachment { + /** + * The object that replaces this `ExternalAttachment` at upload time. + */ + readonly reference: ExternalAttachmentReference; + + private readonly _data: LazyValue; + private readonly state?: BraintrustState; + + /** + * Construct an external attachment. + * + * @param param A parameter object with: + * + * `url`: The fully qualified URL of the file in the external object store. + * + * `filename`: The desired name of the file in Braintrust after uploading. + * This parameter is for visualization purposes only and has no effect on + * attachment storage. + * + * `contentType`: The MIME type of the file. + * + * `state`: (Optional) For internal use. + */ + constructor({ url, filename, contentType, state }: ExternalAttachmentParams) { + super(); + this.reference = { + type: EXTERNAL_ATTACHMENT, + filename, + content_type: contentType, + url, + }; + + this._data = this.initData(); + } + + /** + * For ExternalAttachment, this is a no-op since the data already resides + * in the external object store. It marks the attachment as already uploaded. + * + * @returns The attachment status, which will always indicate success. + */ + async upload(): Promise<{ upload_status: "done" }> { + return { upload_status: "done" as const }; + } + + /** + * The attachment contents. This is a lazy value that will read the attachment contents from the external object store on first access. + */ + async data(): Promise { + return this._data.get(); + } + + /** + * A human-readable description for logging and debugging. + * + * @returns The debug object. The return type is not stable and may change in + * a future release. + */ + debugInfo(): Record { + return { + url: this.reference.url, + reference: this.reference, + state: this.state, + }; + } + + private initData(): LazyValue { + return new LazyValue(async () => { + const readonly = new ReadonlyAttachment(this.reference, this.state); + return await readonly.data(); + }); + } +} + +const attachmentMetadataSchema = z.object({ + downloadUrl: z.string(), + status: attachmentStatusSchema, +}); + +type AttachmentMetadata = { + downloadUrl: string; + status: AttachmentStatus; +}; + +/** + * A readonly alternative to `Attachment`, which can be used for fetching + * already-uploaded Attachments. + */ +export class ReadonlyAttachment { + /** + * Attachment metadata. + */ + readonly reference: AttachmentReference; + + private readonly _data: LazyValue; + private readonly state?: BraintrustState; + + /** + * Construct a ReadonlyAttachment. + * + * @param reference The `AttachmentReference` that should be read by the + * `ReadonlyAttachment` object. + * @param state (Optional) For internal use. + * @returns The new `ReadonlyAttachment` object. + */ + constructor(reference: AttachmentReference, state?: BraintrustState) { + this.reference = reference; + this.state = state; + this._data = this.initDownloader(); + } + + /** + * The attachment contents. This is a lazy value that will read the attachment + * contents from the object store on first access. + */ + async data(): Promise { + return this._data.get(); + } + + /** + * Returns the attachment contents as a base64-encoded URL that is suitable + * for use in a prompt. + * + * @returns The attachment contents as a base64-encoded URL. + */ + async asBase64Url(): Promise { + const buf = await (await this.data()).arrayBuffer(); + const base64 = Buffer.from(buf).toString("base64"); + return `data:${this.reference.content_type};base64,${base64}`; + } + + /** + * Fetch the attachment metadata, which includes a downloadUrl and a status. + * This will re-fetch the status each time in case it changes over time. + */ + async metadata(): Promise { + const state = this.state ?? _internalGetGlobalState(); + await state.login({}); + + const params: Record = { + filename: this.reference.filename, + content_type: this.reference.content_type, + org_id: state.orgId || "", + }; + if (this.reference.type === "braintrust_attachment") { + params.key = this.reference.key; + } else if (this.reference.type === "external_attachment") { + params.url = this.reference.url; + } + const resp = await state.apiConn().get("/attachment", params); + if (!resp.ok) { + const errorStr = JSON.stringify(resp); + throw new Error(`Invalid response from API server: ${errorStr}`); + } + + return attachmentMetadataSchema.parse(await resp.json()); + } + + /** + * Fetch the attachment upload status. This will re-fetch the status each time + * in case it changes over time. + */ + async status(): Promise { + return (await this.metadata()).status; + } + + private initDownloader(): LazyValue { + const download = async () => { + const { downloadUrl, status } = await this.metadata(); + + if (status.upload_status !== "done") { + throw new Error( + `Expected attachment status "done", got "${status.upload_status}"`, + ); + } + + const objResponse = await fetch(downloadUrl); + if (objResponse.status !== 200) { + const error = await objResponse.text(); + throw new Error(`Couldn't download attachment: ${error}`); + } + + return await objResponse.blob(); + }; + + return new LazyValue(download); + } +} + +/** + * Represents a JSON object that should be stored as an attachment. + * + * `JSONAttachment` is a convenience function that creates an `Attachment` + * from JSON data. It's particularly useful for large JSON objects that + * would otherwise bloat the trace size. + * + * The JSON data is automatically serialized and stored as an attachment + * with content type "application/json". + */ +export class JSONAttachment extends Attachment { + /** + * Construct a JSONAttachment from a JSON-serializable object. + * + * @param data The JSON object to attach. Must be JSON-serializable. + * @param options Additional options: + * - `filename`: The filename for the attachment (defaults to "data.json") + * - `pretty`: Whether to pretty-print the JSON (defaults to false) + * - `state`: (Optional) For internal use. + * + * @example + * ```typescript + * const largeTranscript = [ + * { role: "user", content: "..." }, + * { role: "assistant", content: "..." }, + * // ... many more messages + * ]; + * + * logger.log({ + * input: { + * type: "chat", + * transcript: new JSONAttachment(largeTranscript, { filename: "transcript.json" }) + * } + * }); + * ``` + */ + constructor( + data: unknown, + options?: { + filename?: string; + pretty?: boolean; + state?: BraintrustState; + }, + ) { + const { filename = "data.json", pretty = false, state } = options ?? {}; + const deferredJsonAttachment = + globalThis.__BT_DATASET_PIPELINE_DEFER_JSON_ATTACHMENT__; + if (deferredJsonAttachment) { + super({ + data: new Blob([]), + filename, + contentType: "application/json", + state, + }); + return deferredJsonAttachment(data, { + filename, + pretty, + }) as unknown as JSONAttachment; + } + + // Serialize the JSON data + const jsonString = pretty + ? JSON.stringify(data, null, 2) + : JSON.stringify(data); + const blob = new Blob([jsonString], { type: "application/json" }); + + super({ + data: blob, + filename, + contentType: "application/json", + state, + }); + } +} + +export function logFeedbackImpl( + state: BraintrustState, + parentObjectType: SpanObjectTypeV3, + parentObjectId: LazyValue, + { + id, + expected, + scores, + metadata: inputMetadata, + tags, + comment, + source: inputSource, + }: LogFeedbackFullArgs, +): void { + const source = inputSource ?? "external"; + + if (!VALID_SOURCES.includes(source)) { + throw new Error(`source must be one of ${VALID_SOURCES}`); + } + + if ( + isEmpty(scores) && + isEmpty(expected) && + isEmpty(tags) && + isEmpty(comment) + ) { + throw new Error( + "At least one of scores, expected, tags, or comment must be specified", + ); + } + + const validatedEvent = validateAndSanitizeExperimentLogPartialArgs({ + scores, + metadata: inputMetadata, + expected, + tags, + }); + + const { metadata, ...rawUpdateEvent } = deepCopyEvent(validatedEvent); + const updateEvent = Object.fromEntries( + Object.entries(rawUpdateEvent).filter(([_, v]) => !isEmpty(v)), + ); + + const parentIds = async () => + new SpanComponentsV3({ + object_type: parentObjectType, + object_id: await parentObjectId.get(), + }).objectIdFields(); + + if (Object.keys(updateEvent).length > 0) { + const record = new LazyValue(async () => { + return { + id, + ...updateEvent, + ...(await parentIds()), + [AUDIT_SOURCE_FIELD]: source, + [AUDIT_METADATA_FIELD]: metadata, + [IS_MERGE_FIELD]: true, + }; + }); + state.bgLogger().log([record]); + } + + if (!isEmpty(comment)) { + const record = new LazyValue(async () => { + return { + id: uuidv4(), + created: new Date().toISOString(), + origin: { + // NOTE: We do not know (or care?) what the transaction id of the row that + // we're commenting on is here, so we omit it. + id, + }, + comment: { + text: comment, + }, + ...(await parentIds()), + [AUDIT_SOURCE_FIELD]: source, + [AUDIT_METADATA_FIELD]: metadata, + }; + }); + state.bgLogger().log([record]); + } +} + +export function updateSpanImpl({ + state, + parentObjectType, + parentObjectId, + id, + root_span_id, + span_id, + event, +}: { + state: BraintrustState; + parentObjectType: SpanObjectTypeV3; + parentObjectId: LazyValue; + id: string; + root_span_id: string | undefined; + span_id: string | undefined; + event: Omit, "id" | "root_span_id" | "span_id">; +}): void { + if (isEmpty(root_span_id) !== isEmpty(span_id)) { + throw new Error("both root_span_id and span_id must be set, or neither"); + } + const hasExplicitSpanIds = + root_span_id !== undefined && span_id !== undefined; + const updateEvent = deepCopyEvent( + validateAndSanitizeExperimentLogPartialArgs({ + ...event, + id, + ...(hasExplicitSpanIds ? { root_span_id, span_id } : {}), + } as Partial), + ); + + const parentIds = async () => + new SpanComponentsV3({ + object_type: parentObjectType, + object_id: await parentObjectId.get(), + }).objectIdFields(); + + const record = new LazyValue( + async () => + customizeSpanExport({ + id, + ...updateEvent, + ...(await parentIds()), + [IS_MERGE_FIELD]: true, + }) as BackgroundLogEvent | null, + ); + state.bgLogger().log([record]); +} + +/** + * Update a span using the output of `span.export()`. It is important that you only resume updating + * to a span once the original span has been fully written and flushed, since otherwise updates to + * the span may conflict with the original span. + * + * @param exported The output of `span.export()`. + * @param event The event data to update the span with. See {@link Experiment.log} for a full list of valid fields. + * @param state (optional) Login state to use. If not provided, the global state will be used. + */ +export function updateSpan({ + exported, + state, + ...event +}: { exported: string } & Omit< + Partial, + "id" | "root_span_id" | "span_id" +> & + OptionalStateArg): void { + const resolvedState = state ?? _internalGetGlobalState(); + const components = SpanComponentsV4.fromStr(exported); + + if (!components.data.row_id) { + throw new Error("Exported span must have a row id"); + } + + updateSpanImpl({ + state: resolvedState, + parentObjectType: components.data.object_type, + parentObjectId: new LazyValue( + spanComponentsToObjectIdLambda(resolvedState, components), + ), + id: components.data.row_id, + root_span_id: components.data.root_span_id, + span_id: components.data.span_id, + event, + }); +} + +/** @internal Rehydrate an exported root span so work can continue in another process. */ +export function _internalResumeSpan({ + exported, + state, +}: { + exported: string; + state?: BraintrustState; +}): Span { + const resolvedState = state ?? _internalGetGlobalState(); + const components = SpanComponentsV4.fromStr(exported); + const { row_id, root_span_id, span_id } = components.data; + if (!row_id || !root_span_id || !span_id) { + throw new Error("Only exported root spans can be resumed"); + } + return new SpanImpl({ + state: resolvedState, + parentObjectType: components.data.object_type, + parentObjectId: new LazyValue( + spanComponentsToObjectIdLambda(resolvedState, components), + ), + parentComputeObjectMetadataArgs: undefined, + parentSpanIds: { parentSpanIds: [], rootSpanId: root_span_id }, + spanId: span_id, + event: { id: row_id }, + propagatedEvent: (components.data.propagated_event ?? undefined) as + | StartSpanEventArgs + | undefined, + [RESUME_SPAN_WITHOUT_INITIAL_WRITE]: true, + }); +} + +/** + * An opaque W3C trace-context, as returned by + * {@link extractTraceContextFromHeaders}. + * + * Carries the relevant W3C headers (`traceparent`, `baggage`, `tracestate`). + * Callers MUST treat it as opaque and pass it straight to + * `startSpan({ parent })`. + */ +export type PropagationContext = Record; + +export interface ParentSpanIds { + spanId: string; + rootSpanId: string; +} + +export interface MultiParentSpanIds { + parentSpanIds: string[]; + rootSpanId: string; +} + +function spanComponentsToObjectIdLambda( + state: BraintrustState, + components: SpanComponentsV3, +): () => Promise { + if (components.data.object_id) { + const ret = components.data.object_id; + return async () => ret; + } + if (!components.data.compute_object_metadata_args) { + throw new Error( + "Impossible: must provide either objectId or computeObjectMetadataArgs", + ); + } + switch (components.data.object_type) { + case SpanObjectTypeV3.EXPERIMENT: + throw new Error( + "Impossible: computeObjectMetadataArgs not supported for experiments", + ); + case SpanObjectTypeV3.PLAYGROUND_LOGS: + throw new Error( + "Impossible: computeObjectMetadataArgs not supported for prompt sessions", + ); + case SpanObjectTypeV3.PROJECT_LOGS: + return async () => + ( + await computeLoggerMetadata(state, { + ...components.data.compute_object_metadata_args, + }) + ).project.id; + default: + const x: never = components.data.object_type; + throw new Error(`Unknown object type: ${x}`); + } +} + +// Utility function to resolve the object ID of a SpanComponentsV3 object. This +// function may trigger a login to braintrust if the object ID is encoded +// "lazily". +export async function spanComponentsToObjectId({ + components, + state, +}: { + components: SpanComponentsV3; + state?: BraintrustState; +}): Promise { + return await spanComponentsToObjectIdLambda( + state ?? _internalGetGlobalState(), + components, + )(); +} + +export const ERR_PERMALINK = "https://braintrust.dev/error-generating-link"; + +function getErrPermlink(msg: string) { + if (msg == "") { + return ERR_PERMALINK; + } + return `${ERR_PERMALINK}?msg=${encodeURIComponent(msg)}`; +} + +function _getAppUrl(appUrl?: string | null): string { + return ( + appUrl || iso.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev" + ); +} + +function _getOrgName(orgName?: string | null): string | undefined { + return orgName || iso.getEnv("BRAINTRUST_ORG_NAME") || undefined; +} + +/** + * Return the base URL for links (e.g. https://braintrust.dev/app/my-org-name) + * if we have the info, otherwise return null. + * Resolution order: state -> linkArgs -> env var + */ +function _getLinkBaseUrl( + state: BraintrustState, + linkArgs?: LinkArgs, +): string | null { + const appUrl = _getAppUrl(state.appUrl || linkArgs?.app_url); + const orgName = _getOrgName(state.orgName || linkArgs?.org_name); + if (!orgName) { + return null; + } + return `${appUrl}/app/${orgName}`; +} + +/** + * Format a permalink to the Braintrust application for viewing the span + * represented by the provided `slug`. + * + * Links can be generated at any time, but they will only become viewable after + * the span and its root have been flushed to the server and ingested. + * + * If you have a `Span` object, use {@link Span.link} instead. + * + * @param slug The identifier generated from {@link Span.export}. + * @param opts Optional arguments. + * @param opts.state The login state to use. If not provided, the global state will be used. + * @param opts.orgName The org name to use. If not provided, the org name will be inferred from the state. + * @param opts.appUrl The app URL to use. If not provided, the app URL will be inferred from the state. + * @returns A permalink to the exported span. + */ +export async function permalink( + slug: string, + opts?: { + state?: BraintrustState; + orgName?: string; + appUrl?: string; + }, +): Promise { + // Noop spans have an empty slug, so return a dummy permalink. + if (slug === "") { + return NOOP_SPAN_PERMALINK; + } + + const state = opts?.state ?? _internalGetGlobalState(); + const getOrgName = async () => { + if (opts?.orgName) { + return opts.orgName; + } + await state.login({}); + if (!state.orgName) { + throw new Error("provide-org-or-login"); // this is caught below + } + return state.orgName; + }; + const getAppUrl = async () => { + if (opts?.appUrl) { + return opts.appUrl; + } + await state.login({}); + if (!state.appUrl) { + throw new Error("provide-app-url-or-login"); // this is caught below + } + return state.appUrl; + }; + + try { + const components = SpanComponentsV4.fromStr(slug); + const object_type = spanObjectTypeV3ToString(components.data.object_type); + const [orgName, appUrl, object_id] = await Promise.all([ + getOrgName(), + getAppUrl(), + spanComponentsToObjectId({ components, state }), + ]); + const id = components.data.row_id; + if (!id) { + throw new Error("Span slug does not refer to an individual row"); + } + const urlParams = new URLSearchParams({ object_type, object_id, id }); + return `${appUrl}/app/${orgName}/object?${urlParams}`; + } catch (e) { + if (e instanceof FailedHTTPResponse) { + return getErrPermlink(`http-error-${e.status}`); + } + return getErrPermlink(e instanceof Error ? e.message : String(e)); + } +} + +// IMPORTANT NOTE: This function may pass arguments which override those in the +// main argument set, so if using this in a spread, like `SpanImpl({ ...args, +// ...startSpanParentArgs(...)})`, make sure to put startSpanParentArgs after +// the original argument set. +export function startSpanParentArgs(args: { + state: BraintrustState; + // `parent` may be an exported slug string or an opaque W3C trace-context + // (from `extractTraceContextFromHeaders`). + parent: string | PropagationContext | undefined; + parentObjectType: SpanObjectTypeV3; + parentObjectId: LazyValue; + parentComputeObjectMetadataArgs: Record | undefined; + parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined; + propagatedEvent: StartSpanEventArgs | undefined; + propagatedState?: PropagatedState | undefined; +}): { + parentObjectType: SpanObjectTypeV3; + parentObjectId: LazyValue; + parentComputeObjectMetadataArgs: Record | undefined; + parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined; + propagatedEvent: StartSpanEventArgs | undefined; + propagatedState: PropagatedState | undefined; +} { + let argParentObjectId: LazyValue | undefined = undefined; + let argParentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined = + undefined; + let argPropagatedEvent: StartSpanEventArgs | undefined = undefined; + let argPropagatedState: PropagatedState | undefined = undefined; + const { parentSlug, propagatedState: parentPropagatedState } = + normalizeParent(args.parent, args.state); + if (parentSlug) { + if (args.parentSpanIds) { + throw new Error("Cannot specify both parent and parentSpanIds"); + } + const parentComponents = SpanComponentsV4.fromStr(parentSlug); + if (args.parentObjectType !== parentComponents.data.object_type) { + throw new Error( + `Mismatch between expected span parent object type ${args.parentObjectType} and provided type ${parentComponents.data.object_type}`, + ); + } + + // For logger/span method calls that receive a W3C parent, the inbound + // context links this span to the upstream trace ids, but the span still + // belongs to the current Braintrust project/experiment and should propagate + // that current parent onward. Keep the receiver's object id/metadata and + // only forward opaque W3C state (trace-flags/tracestate), not the inbound + // braintrust.parent fallback. + argParentObjectId = args.parentObjectId; + if ( + parentComponents.data.row_id && + parentSpanIdsUsable( + parentComponents.data.span_id, + parentComponents.data.root_span_id, + ) + ) { + argParentSpanIds = { + spanId: parentComponents.data.span_id, + rootSpanId: parentComponents.data.root_span_id, + }; + } + argPropagatedEvent = + args.propagatedEvent ?? + ((parentComponents.data.propagated_event ?? undefined) as + | StartSpanEventArgs + | undefined); + const propagatedState = args.propagatedState ?? parentPropagatedState; + if (propagatedState) { + const { braintrustParent: _ignoredBraintrustParent, ...w3cState } = + propagatedState; + argPropagatedState = w3cState; + } + } else { + argParentObjectId = args.parentObjectId; + argParentSpanIds = args.parentSpanIds; + argPropagatedEvent = args.propagatedEvent; + argPropagatedState = args.propagatedState; + } + + return { + parentObjectType: args.parentObjectType, + parentObjectId: argParentObjectId, + parentComputeObjectMetadataArgs: args.parentComputeObjectMetadataArgs, + parentSpanIds: argParentSpanIds, + propagatedEvent: argPropagatedEvent, + propagatedState: argPropagatedState, + }; +} + +export class Logger implements Exportable { + private state: BraintrustState; + private lazyMetadata: LazyValue; + private computeMetadataArgs: ProjectMetadataArgs | undefined; + private linkArgs: LinkArgs | undefined; + private lastStartTime: number; + private lazyId: LazyValue; + private calledStartSpan: boolean; + private readonly captureAttachments: boolean | undefined; + + // For type identification. + public kind = "logger" as const; + + constructor( + state: BraintrustState, + lazyMetadata: LazyValue, + options: { + computeMetadataArgs?: ProjectMetadataArgs; + linkArgs?: LinkArgs; + captureAttachments?: boolean; + } = {}, + ) { + this.lazyMetadata = lazyMetadata; + this.computeMetadataArgs = options.computeMetadataArgs; + this.linkArgs = options.linkArgs; + this.lastStartTime = getCurrentUnixTimestamp(); + this.lazyId = new LazyValue(() => this.id); + this.calledStartSpan = false; + this.captureAttachments = options.captureAttachments; + this.state = state; + } + + public get org_id(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).org_id; + })(); + } + + public get project(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).project; + })(); + } + + public get id(): Promise { + return (async () => (await this.project).id)(); + } + + public get loggingState(): BraintrustState { + return this.state; + } + + static { + Object.defineProperty(this.prototype, CAPTURE_ATTACHMENTS, { + configurable: true, + get(this: Logger): boolean { + return this.captureAttachments ?? captureAttachmentsFromEnv(); + }, + }); + } + + private parentObjectType() { + return SpanObjectTypeV3.PROJECT_LOGS; + } + + /** + * Log a single event. The event will be batched and uploaded behind the scenes. Call and await {@link Logger.flush} to ensure the event has been uploaded. + * + * @param event The event to log. + * @param event.input: (Optional) the arguments that uniquely define a user input (an arbitrary, JSON serializable object). + * @param event.output: (Optional) the output of your application, including post-processing (an arbitrary, JSON serializable object), that allows you to determine whether the result is correct or not. For example, in an app that generates SQL queries, the `output` should be the _result_ of the SQL query generated by the model, not the query itself, because there may be multiple valid queries that answer a single question. + * @param event.expected: (Optional) the ground truth value (an arbitrary, JSON serializable object) that you'd compare to `output` to determine if your `output` value is correct or not. Braintrust currently does not compare `output` to `expected` for you, since there are so many different ways to do that correctly. Instead, these values are just used to help you navigate while digging into analyses. However, we may later use these values to re-score outputs or fine-tune your models. + * @param event.error: (Optional) The error that occurred, if any. If you use tracing to run an experiment, errors are automatically logged when your code throws an exception. + * @param event.scores: (Optional) a dictionary of numeric values (between 0 and 1) to log. The scores should give you a variety of signals that help you determine how accurate the outputs are compared to what you expect and diagnose failures. For example, a summarization app might have one score that tells you how accurate the summary is, and another that measures the word similarity between the generated and grouth truth summary. The word similarity score could help you determine whether the summarization was covering similar concepts or not. You can use these scores to help you sort, filter, and compare logs. + * @param event.metadata: (Optional) a dictionary with additional data about the test example, model outputs, or just about anything else that's relevant, that you can use to help find and analyze examples later. For example, you could log the `prompt`, example's `id`, or anything else that would be useful to slice/dice later. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. + * @param event.metrics: (Optional) a dictionary of metrics to log. The following keys are populated automatically: "start", "end". + * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you. + * @param options Additional logging options + * @param options.allowConcurrentWithSpans in rare cases where you need to log at the top level separately from spans on the logger elsewhere, set this to true. + * @returns The `id` of the logged event. + */ + public log( + event: Readonly, + options?: { allowConcurrentWithSpans?: boolean }, + ): string { + if (this.calledStartSpan && !options?.allowConcurrentWithSpans) { + throw new Error( + "Cannot run toplevel `log` method while using spans. To log to the span, call `logger.traced` and then log with `span.log`", + ); + } + + const span = this.startSpanImpl({ startTime: this.lastStartTime, event }); + this.lastStartTime = span.end(); + return span.id; + } + + /** + * Create a new toplevel span underneath the logger. The name defaults to "root". + * + * See {@link Span.traced} for full details. + */ + public traced( + callback: (span: Span) => R, + args?: StartSpanArgs & SetCurrentArg, + ): R { + const { setCurrent, ...argsRest } = args ?? {}; + const span = this.startSpan(argsRest); + + return runCatchFinally( + () => { + if (setCurrent ?? true) { + return withCurrent(span, callback); + } else { + return callback(span); + } + }, + (error) => { + logError(span, error); + throw error; + }, + () => span.end(), + ); + } + + /** + * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations + * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", + * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead. + * + * See {@link traced} for full details. + */ + public startSpan(args?: StartSpanArgs): Span { + this.calledStartSpan = true; + return this.startSpanImpl(args); + } + + private startSpanImpl(args?: StartSpanArgs): Span { + return new SpanImpl({ + [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], + ...args, + // Sometimes `args` gets passed directly into this function, and it contains an undefined value for `state`. + // To ensure that we always use this logger's state, we override the `state` argument no matter what. + state: this.state, + ...startSpanParentArgs({ + state: this.state, + parent: args?.parent, + parentObjectType: this.parentObjectType(), + parentObjectId: this.lazyId, + parentComputeObjectMetadataArgs: this.computeMetadataArgs, + parentSpanIds: args?.parentSpanIds, + propagatedEvent: args?.propagatedEvent, + }), + defaultRootType: SpanTypeAttribute.TASK, + }); + } + + /** + * Log feedback to an event. Feedback is used to save feedback scores, set an expected value, or add a comment. + * + * @param event + * @param event.id The id of the event to log feedback for. This is the `id` returned by `log` or accessible as the `id` field of a span. + * @param event.scores (Optional) a dictionary of numeric values (between 0 and 1) to log. These scores will be merged into the existing scores for the event. + * @param event.expected (Optional) the ground truth value (an arbitrary, JSON serializable object) that you'd compare to `output` to determine if your `output` value is correct or not. + * @param event.comment (Optional) an optional comment string to log about the event. + * @param event.metadata (Optional) a dictionary with additional data about the feedback. If you have a `user_id`, you can log it here and access it in the Braintrust UI. Note, this metadata does not correspond to the main event itself, but rather the audit log attached to the event. + * @param event.source (Optional) the source of the feedback. Must be one of "external" (default), "app", or "api". + */ + public logFeedback(event: LogFeedbackFullArgs): void { + logFeedbackImpl(this.state, this.parentObjectType(), this.lazyId, event); + } + + /** + * Update a span in the experiment using its id. It is important that you only update a span once the original span has been fully written and flushed, + * since otherwise updates to the span may conflict with the original span. + * + * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields. + */ + public updateSpan( + event: Omit, "id"> & + Required>, + ): void { + const { id, root_span_id, span_id, ...eventRest } = event; + if (!id) { + throw new Error("Span id is required to update a span"); + } + updateSpanImpl({ + state: this.state, + parentObjectType: this.parentObjectType(), + parentObjectId: this.lazyId, + id, + root_span_id, + span_id, + event: eventRest, + }); + } + + /** + * Return a serialized representation of the logger that can be used to start subspans in other places. + * + * See {@link Span.startSpan} for more details. + */ + public async export(): Promise { + // Note: it is important that the object id we are checking for + // `has_computed` is the same as the one we are passing into the span + // logging functions. So that if the spans actually do get logged, then this + // `_lazy_id` object specifically will also be marked as computed. + return new (getSpanComponentsClass())({ + object_type: this.parentObjectType(), + ...(this.computeMetadataArgs && !this.lazyId.hasSucceeded + ? { compute_object_metadata_args: this.computeMetadataArgs } + : { object_id: await this.lazyId.get() }), + }).toStr(); + } + + /* + * Flush any pending logs to the server. + */ + async flush(): Promise { + return await this.state.bgLogger().flush(); + } + + /** + * Return the base URL for links (e.g. https://braintrust.dev/app/my-org-name) + * if we have the info, otherwise return null. + * Resolution order: state -> linkArgs -> env var + */ + public _getLinkBaseUrl(): string | null { + return _getLinkBaseUrl(this.state, this.linkArgs); + } + + /** + * Return this logger's Braintrust parent string (`project_id:` or + * `project_name:`) for the `braintrust.parent` baggage entry, or + * undefined when it cannot be determined synchronously. + */ + public _getOtelParent(): string | undefined { + const id = + this.computeMetadataArgs?.project_id || this.lazyId.getSync().value; + if (id) { + return `project_id:${id}`; + } + const name = this.computeMetadataArgs?.project_name; + if (name) { + return `project_name:${name}`; + } + return undefined; + } +} + +export type Logs3OverflowUpload = { + method: "PUT" | "POST"; + signedUrl: string; + headers?: Record; + fields?: Record; + key: string; +}; + +export const logs3OverflowUploadSchema: z.ZodType = + z.object({ + method: z.enum(["PUT", "POST"]), + signedUrl: z.string().url(), + headers: z.record(z.string()).optional(), + fields: z.record(z.string()).optional(), + key: z.string().min(1), + }); + +export type Logs3OverflowInputRow = { + object_ids: Record; + is_delete?: boolean; + input_row: { + byte_size: number; + }; +}; + +type LogItemWithMeta = { + str: string; + overflowMeta: Logs3OverflowInputRow; +}; + +function constructLogs3Data(items: LogItemWithMeta[]) { + return `{"rows": ${constructJsonArray(items.map((i) => i.str))}, "api_version": 2}`; +} + +export function constructLogs3OverflowRequest(key: string): { + rows: { type: string; key: string }; + api_version: number; +} { + return { + rows: { + type: LOGS3_OVERFLOW_REFERENCE_TYPE, + key, + }, + api_version: 2, + }; +} + +export function pickLogs3OverflowObjectIds( + row: Record, +): Record { + const objectIds: Record = {}; + for (const key of OBJECT_ID_KEYS) { + if (key in row) { + objectIds[key] = row[key]; + } + } + return objectIds; +} + +/** + * Upload a logs3 overflow payload to the signed URL. + * This is a standalone function that can be used by both SDK and app code. + * + * @param upload - The overflow upload metadata from the API + * @param payload - The JSON payload string to upload + * @param fetchFn - Optional custom fetch function (defaults to global fetch) + */ +export async function uploadLogs3OverflowPayload( + upload: Logs3OverflowUpload, + payload: string, + fetchFn: typeof fetch = fetch, +): Promise { + if (upload.method === "POST") { + if (!upload.fields) { + throw new Error("Missing logs3 overflow upload fields"); + } + if (typeof FormData === "undefined" || typeof Blob === "undefined") { + throw new Error("FormData is not available for logs3 overflow upload"); + } + const form = new FormData(); + for (const [key, value] of Object.entries(upload.fields)) { + form.append(key, value); + } + const contentType = upload.fields["Content-Type"] ?? "application/json"; + form.append("file", new Blob([payload], { type: contentType })); + const headers: Record = {}; + for (const [key, value] of Object.entries(upload.headers ?? {})) { + if (key.toLowerCase() !== "content-type") { + headers[key] = value; + } + } + const response = await fetchFn(upload.signedUrl, { + method: "POST" satisfies RequestInit["method"], + headers, + body: form, + }); + if (!response.ok) { + const responseText = await response.text().catch(() => ""); + throw new Error( + `Failed to upload logs3 overflow payload: ${response.status} ${responseText}`, + ); + } + return; + } + const headers: Record = { ...(upload.headers ?? {}) }; + addAzureBlobHeaders(headers, upload.signedUrl); + const response = await fetchFn(upload.signedUrl, { + method: "PUT" satisfies RequestInit["method"], + headers, + body: payload, + }); + if (!response.ok) { + const responseText = await response.text().catch(() => ""); + throw new Error( + `Failed to upload logs3 overflow payload: ${response.status} ${responseText}`, + ); + } +} + +function stringifyWithOverflowMeta(item: object): LogItemWithMeta { + const str = JSON.stringify(item); + const record = item as Record; + return { + str, + overflowMeta: { + object_ids: pickLogs3OverflowObjectIds(record), + is_delete: record[OBJECT_DELETE_FIELD] === true, + input_row: { + byte_size: utf8ByteLength(str), + }, + }, + }; +} + +export function utf8ByteLength(value: string): number { + if (typeof TextEncoder !== "undefined") { + return new TextEncoder().encode(value).length; + } + return value.length; +} + +function now() { + return new Date().getTime(); +} + +export interface BackgroundLoggerOpts { + noExitFlush?: boolean; + onFlushError?: (error: unknown) => void; +} + +const DEFAULT_FLUSH_BACKPRESSURE_BYTES = 10 * 1024 * 1024; +// 10 MB +const DEFAULT_MAX_CONCURRENT_LOG_REQUESTS = 8; + +// Read a numeric environment variable. Unset, empty, whitespace-only and +// non-numeric values return undefined so the caller keeps its default, rather +// than being read as 0 (`Number("") === 0`). +function getNumericEnv(name: string): number | undefined { + const raw = iso.getEnv(name); + if (raw === undefined || raw === null || raw.trim() === "") { + return undefined; + } + const value = Number(raw); + return isNaN(value) ? undefined : value; +} + +interface BackgroundLogger { + log(items: LazyValue[]): void; + flush(): Promise; + pendingFlushBytes(): number; + flushBackpressureBytes(): number; + setMaskingFunction( + maskingFunction: ((value: unknown) => unknown) | null, + ): void; +} + +export class TestBackgroundLogger implements BackgroundLogger { + private items: LazyValue[][] = []; + private exportCustomizers: readonly SpanCustomizer[] = []; + + log(items: LazyValue[]): void { + this.items.push(items); + } + + setMaskingFunction( + maskingFunction: ((value: unknown) => unknown) | null, + ): void { + this.exportCustomizers = maskingFunction + ? [createMaskingCustomizer(maskingFunction)] + : []; + } + + async flush(): Promise { + return Promise.resolve(); + } + + pendingFlushBytes(): number { + return 0; + } + + flushBackpressureBytes(): number { + return DEFAULT_FLUSH_BACKPRESSURE_BYTES; + } + + async drain(): Promise { + const items = this.items; + this.items = []; + + // get all the values + const events: BackgroundLogEvent[] = []; + for (const item of items) { + for (const event of item) { + const value = await event.get(); + if (value !== null) events.push(value); + } + } + + let batch = mergeRowBatch(events); + + if (this.exportCustomizers.length) { + batch = batch + .map( + (item) => + customizeSpanExport( + item as SpanExportData, + this.exportCustomizers, + ) as BackgroundLogEvent | null, + ) + .filter((item): item is BackgroundLogEvent => item !== null); + } + + return batch; + } +} + +const BACKGROUND_LOGGER_BASE_SLEEP_TIME_S = 1.0; + +const HTTP_RETRY_BASE_SLEEP_TIME_S = 1.0; + +const HTTP_RETRY_JITTER_MS = 200; + +export const BTQL_HTTP_RETRIES = 3; + +const RETRYABLE_HTTP_STATUS_CODES = new Set([500, 502, 503, 504]); + +function isRetryableHTTPError(error: unknown): boolean { + return ( + !(error instanceof FailedHTTPResponse) || + RETRYABLE_HTTP_STATUS_CODES.has(error.status) + ); +} + +function formatHTTPError(error: unknown): string { + if (error instanceof FailedHTTPResponse) { + return `${error.status} ${error.text}`; + } + return error instanceof Error ? error.message : String(error); +} + +function getAbortReason(signal: AbortSignal): unknown { + return signal.reason ?? new Error("Request aborted"); +} + +async function waitForRetry( + delayMs: number, + signal?: AbortSignal | null, +): Promise { + if (!signal) { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + return; + } + + if (signal.aborted) { + throw getAbortReason(signal); + } + + await new Promise((resolve, reject) => { + const onAbort = () => { + clearTimeout(timeout); + signal.removeEventListener("abort", onAbort); + reject(getAbortReason(signal)); + }; + const timeout = setTimeout(() => { + signal.removeEventListener("abort", onAbort); + resolve(); + }, delayMs); + signal.addEventListener("abort", onAbort, { once: true }); + if (signal.aborted) { + onAbort(); + } + }); +} + +// We should only have one instance of this object per state object in +// 'BraintrustState._bgLogger'. Be careful about spawning multiple +// instances of this class, because concurrent BackgroundLoggers will not log to +// the backend in a deterministic order. +class ConcurrencyLimiter { + private activeCount = 0; + private readonly waiters: Array<() => void> = []; + + constructor(private readonly limit: number) {} + + async run(callback: () => Promise): Promise { + if (this.activeCount >= this.limit) { + await new Promise((resolve) => this.waiters.push(resolve)); + } else { + this.activeCount++; + } + + try { + return await callback(); + } finally { + const next = this.waiters.shift(); + if (next) { + next(); + } else { + this.activeCount--; + } + } + } +} + +interface QueuedLogEvent { + sequence: number; + event: LazyValue; +} + +class HTTPBackgroundLogger implements BackgroundLogger { + private apiConn: LazyValue; + private queue: Queue; + private activeFlush: Promise = Promise.resolve(); + private activeFlushResolved = true; + private onFlushError?: (error: unknown) => void; + private exportCustomizers: readonly SpanCustomizer[] = []; + private readonly requestLimiter: ConcurrencyLimiter; + private lastEnqueuedSequence = 0; + private completedSequence = 0; + + public syncFlush: boolean = false; + private maxRequestSizeOverride: number | null = null; + private _maxRequestSizePromise: Promise<{ + maxRequestSize: number; + canUseOverflow: boolean; + }> | null = null; + public defaultBatchSize: number = 100; + public numTries: number = 3; + public queueDropExceedingMaxsize: number = DEFAULT_QUEUE_SIZE; + public queueDropLoggingPeriod: number = 60; + public failedPublishPayloadsDir: string | undefined = undefined; + public allPublishPayloadsDir: string | undefined = undefined; + private _flushBackpressureBytes: number = DEFAULT_FLUSH_BACKPRESSURE_BYTES; + + private _pendingBytes: number = 0; + + private _disabled = false; + + private queueDropLoggingState = { + numDropped: 0, + lastLoggedTimestamp: 0, + }; + + constructor(apiConn: LazyValue, opts?: BackgroundLoggerOpts) { + opts = opts ?? {}; + this.apiConn = apiConn; + + const syncFlushEnv = getNumericEnv("BRAINTRUST_SYNC_FLUSH"); + if (syncFlushEnv !== undefined) { + this.syncFlush = Boolean(syncFlushEnv); + } + + const defaultBatchSizeEnv = getNumericEnv("BRAINTRUST_DEFAULT_BATCH_SIZE"); + if (defaultBatchSizeEnv !== undefined) { + this.defaultBatchSize = defaultBatchSizeEnv; + } + + const maxRequestSizeEnv = getNumericEnv("BRAINTRUST_MAX_REQUEST_SIZE"); + if (maxRequestSizeEnv !== undefined) { + this.maxRequestSizeOverride = maxRequestSizeEnv; + } + + const numTriesEnv = getNumericEnv("BRAINTRUST_NUM_RETRIES"); + if (numTriesEnv !== undefined) { + this.numTries = numTriesEnv + 1; + } + + const queueDropExceedingMaxsizeEnv = getNumericEnv( + "BRAINTRUST_QUEUE_DROP_EXCEEDING_MAXSIZE", + ); + + if (queueDropExceedingMaxsizeEnv !== undefined) { + this.queueDropExceedingMaxsize = queueDropExceedingMaxsizeEnv; + } + + this.queue = new Queue(this.queueDropExceedingMaxsize); + + const maxConcurrentLogRequests = + getNumericEnv("BRAINTRUST_MAX_CONCURRENT_LOG_REQUESTS") ?? + DEFAULT_MAX_CONCURRENT_LOG_REQUESTS; + if ( + !Number.isInteger(maxConcurrentLogRequests) || + maxConcurrentLogRequests < 1 + ) { + debugLogger.warn( + `maxConcurrentLogRequests ${maxConcurrentLogRequests} is not a positive integer, using default ${DEFAULT_MAX_CONCURRENT_LOG_REQUESTS}`, + ); + this.requestLimiter = new ConcurrencyLimiter( + DEFAULT_MAX_CONCURRENT_LOG_REQUESTS, + ); + } else { + this.requestLimiter = new ConcurrencyLimiter(maxConcurrentLogRequests); + } + + const queueDropLoggingPeriodEnv = getNumericEnv( + "BRAINTRUST_QUEUE_DROP_LOGGING_PERIOD", + ); + if (queueDropLoggingPeriodEnv !== undefined) { + this.queueDropLoggingPeriod = queueDropLoggingPeriodEnv; + } + + if (iso.getEnv("BRAINTRUST_LOG_FLUSH_CHUNK_SIZE")) { + debugLogger.warn( + "BRAINTRUST_LOG_FLUSH_CHUNK_SIZE is deprecated and no longer has any effect. " + + "Log flushing now sends all items at once and batches them automatically. " + + "This environment variable will be removed in a future major release.", + ); + } + + const flushBackpressureBytesEnv = getNumericEnv( + "BRAINTRUST_FLUSH_BACKPRESSURE_BYTES", + ); + if ( + flushBackpressureBytesEnv !== undefined && + flushBackpressureBytesEnv > 0 + ) { + this._flushBackpressureBytes = flushBackpressureBytesEnv; + } + + const failedPublishPayloadsDirEnv = iso.getEnv( + "BRAINTRUST_FAILED_PUBLISH_PAYLOADS_DIR", + ); + if (failedPublishPayloadsDirEnv) { + this.failedPublishPayloadsDir = failedPublishPayloadsDirEnv; + } + + const allPublishPayloadsDirEnv = iso.getEnv( + "BRAINTRUST_ALL_PUBLISH_PAYLOADS_DIR", + ); + if (allPublishPayloadsDirEnv) { + this.allPublishPayloadsDir = allPublishPayloadsDirEnv; + } + + // Note that this will not run for explicit termination events, such as + // calls to `process.exit()` or uncaught exceptions. Thus it is a + // "best-effort" flush. + if (!opts.noExitFlush) { + iso.processOn("beforeExit", async () => { + await this.flush(); + }); + } + this.onFlushError = opts.onFlushError; + } + + setMaskingFunction( + maskingFunction: ((value: unknown) => unknown) | null, + ): void { + this.exportCustomizers = maskingFunction + ? [createMaskingCustomizer(maskingFunction)] + : []; + } + + pendingFlushBytes(): number { + return this._pendingBytes; + } + + flushBackpressureBytes(): number { + return this._flushBackpressureBytes; + } + + log(items: LazyValue[]): void { + if (this._disabled) { + return; + } + + const queuedItems = items.map((event) => ({ + sequence: ++this.lastEnqueuedSequence, + event, + })); + const droppedItems = this.queue.push(...queuedItems); + // Queue limits reject a suffix of newly pushed items, so their sequence + // numbers can be reused by the next accepted events. + this.lastEnqueuedSequence -= droppedItems.length; + + if (!this.syncFlush) { + this.triggerActiveFlush(); + } + + if (droppedItems.length) { + this.registerDroppedItemCount(droppedItems.length); + if (this.allPublishPayloadsDir || this.failedPublishPayloadsDir) { + this.dumpDroppedEvents(droppedItems.map((item) => item.event)); + } + } + } + + private getMaxRequestSize(): Promise<{ + maxRequestSize: number; + canUseOverflow: boolean; + }> { + if (!this._maxRequestSizePromise) { + this._maxRequestSizePromise = (async () => { + let serverLimit: number | null = null; + try { + const conn = await this.apiConn.get(); + const versionInfo = await conn.get_json("version"); + serverLimit = + z + .object({ logs3_payload_max_bytes: z.number().nullish() }) + .parse(versionInfo).logs3_payload_max_bytes ?? null; + } catch (e) { + debugLogger.warn( + "Failed to fetch version info for payload limit:", + e, + ); + } + const validServerLimit = + serverLimit !== null && serverLimit > 0 ? serverLimit : null; + const canUseOverflow = validServerLimit !== null; + let maxRequestSize = DEFAULT_MAX_REQUEST_SIZE; + if (this.maxRequestSizeOverride !== null) { + maxRequestSize = + validServerLimit !== null + ? Math.min(this.maxRequestSizeOverride, validServerLimit) + : this.maxRequestSizeOverride; + } else if (validServerLimit !== null) { + maxRequestSize = validServerLimit; + } + return { maxRequestSize, canUseOverflow }; + })(); + } + return this._maxRequestSizePromise; + } + + async flush(): Promise { + const targetSequence = this.lastEnqueuedSequence; + + while (this.completedSequence < targetSequence) { + this.triggerActiveFlush(); + await this.activeFlush; + } + } + + private async flushThrough( + flushBoundary: number, + args?: { batchSize?: number }, + ): Promise { + const batchSize = args?.batchSize ?? this.defaultBatchSize; + + const queuedItems = this.queue.drainWhile( + (item) => item.sequence <= flushBoundary, + ); + if (queuedItems.length === 0) { + return; + } + + try { + if (!this._disabled) { + await this.flushWrappedItemsChunk( + queuedItems.map((item) => item.event), + batchSize, + ); + } + } catch (error) { + this.reportFlushError(error); + } finally { + this.completedSequence = queuedItems[queuedItems.length - 1].sequence; + } + } + + private async flushWrappedItemsChunk( + wrappedItems: LazyValue[], + batchSize: number, + ) { + if (!wrappedItems.length) { + return; + } + + const [allItems, attachments] = await this.unwrapLazyValues(wrappedItems); + if (allItems.length === 0) { + return; + } + + // Construct batches of records to flush in parallel. + let chunkBytes = 0; + const allItemsWithMeta = allItems.map((item) => { + const withMeta = stringifyWithOverflowMeta(item); + chunkBytes += withMeta.str.length; + return withMeta; + }); + this._pendingBytes += chunkBytes; + + const maxRequestSizeResult = await this.getMaxRequestSize(); + const batches = batchItems({ + items: allItemsWithMeta, + batchMaxNumItems: batchSize, + batchMaxNumBytes: maxRequestSizeResult.maxRequestSize / 2, + getByteSize: (item) => item.str.length, + }); + + const postPromises = batches.map((batch) => + this.requestLimiter.run(async () => { + try { + await this.submitLogsRequest(batch, maxRequestSizeResult); + return { type: "success" } as const; + } catch (e) { + return { type: "error", value: e } as const; + } + }), + ); + const results = await Promise.all(postPromises); + this._pendingBytes = Math.max(0, this._pendingBytes - chunkBytes); + const failingResultErrors = results + .map((r) => (r.type === "success" ? undefined : r.value)) + .filter((r) => r !== undefined); + if (failingResultErrors.length) { + throw new AggregateError( + failingResultErrors, + `Encountered the following errors while logging:`, + ); + } + + const attachmentErrors: unknown[] = []; + // For now, upload attachments serially. + for (const attachment of attachments) { + try { + const result = await attachment.upload(); + if (result.upload_status === "error") { + throw new Error(result.error_message); + } + } catch (error) { + attachmentErrors.push(error); + } + } + if (attachmentErrors.length === 1) { + throw attachmentErrors[0]; + } else if (attachmentErrors.length > 1) { + throw new AggregateError( + attachmentErrors, + `Encountered the following errors while uploading attachments:`, + ); + } + } + + private async unwrapLazyValues( + wrappedItems: LazyValue[], + ): Promise<[BackgroundLogEvent[], Attachment[]]> { + for (let i = 0; i < this.numTries; ++i) { + try { + const items = ( + await Promise.all(wrappedItems.map((x) => x.get())) + ).filter((item): item is BackgroundLogEvent => item !== null); + + // TODO(kevin): `extractAttachments` should ideally come after + // `mergeRowBatch`, since merge-overwriting could result in some + // attachments that no longer need to be uploaded. This would require + // modifying the merge logic to treat the Attachment object as a "leaf" + // rather than attempting to merge the keys of the Attachment. + const attachments: Attachment[] = []; + items.forEach((item) => extractAttachments(item, attachments)); + + let mergedItems = mergeRowBatch(items); + + if (this.exportCustomizers.length) { + mergedItems = mergedItems + .map( + (item) => + customizeSpanExport( + item as SpanExportData, + this.exportCustomizers, + ) as BackgroundLogEvent | null, + ) + .filter((item): item is BackgroundLogEvent => item !== null); + } + + return [mergedItems, attachments]; + } catch (e) { + let errmsg = "Encountered error when constructing records to flush"; + const isRetrying = i + 1 < this.numTries; + if (isRetrying) { + errmsg += ". Retrying"; + } + + debugLogger.warn(errmsg); + if (!isRetrying) { + debugLogger.warn( + `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`, + ); + throw e; + } else { + debugLogger.warn(e); + const sleepTimeS = BACKGROUND_LOGGER_BASE_SLEEP_TIME_S * 2 ** i; + debugLogger.info(`Sleeping for ${sleepTimeS}s`); + await new Promise((resolve) => + setTimeout(resolve, sleepTimeS * 1000), + ); + } + } + } + throw new Error("Impossible"); + } + + private async requestLogs3OverflowUpload( + conn: HTTPConnection, + args: { rows: Logs3OverflowInputRow[]; sizeBytes: number }, + ): Promise { + let response: unknown; + try { + response = await conn.post_json("logs3/overflow", { + content_type: "application/json", + size_bytes: args.sizeBytes, + rows: args.rows, + }); + } catch (error) { + const errorStr = JSON.stringify(error); + throw new Error( + `Failed to request logs3 overflow upload URL: ${errorStr}`, + ); + } + + try { + return logs3OverflowUploadSchema.parse(response); + } catch (error) { + if (error instanceof ZodError) { + const errorStr = JSON.stringify(error.flatten()); + throw new Error(`Invalid response from API server: ${errorStr}`); + } + throw error; + } + } + + private async _uploadLogs3OverflowPayload( + conn: HTTPConnection, + upload: Logs3OverflowUpload, + payload: string, + ): Promise { + // Delegate to the public function, using the connection's fetch + await uploadLogs3OverflowPayload(upload, payload, conn.fetch.bind(conn)); + } + + private async submitLogsRequest( + items: LogItemWithMeta[], + { + maxRequestSize, + canUseOverflow, + }: { maxRequestSize: number; canUseOverflow: boolean }, + ): Promise { + const conn = await this.apiConn.get(); + const dataStr = constructLogs3Data(items); + const payloadBytes = utf8ByteLength(dataStr); + const useOverflow = canUseOverflow && payloadBytes > maxRequestSize; + if (this.allPublishPayloadsDir) { + await HTTPBackgroundLogger.writePayloadToDir({ + payloadDir: this.allPublishPayloadsDir, + payload: dataStr, + }); + } + + let overflowUpload: Logs3OverflowUpload | null = null; + const overflowRows = useOverflow + ? items.map((item) => item.overflowMeta) + : null; + + for (let i = 0; i < this.numTries; i++) { + const startTime = now(); + let error: unknown = undefined; + try { + if (overflowRows) { + if (!overflowUpload) { + const currentUpload = await this.requestLogs3OverflowUpload(conn, { + rows: overflowRows, + sizeBytes: payloadBytes, + }); + await this._uploadLogs3OverflowPayload( + conn, + currentUpload, + dataStr, + ); + overflowUpload = currentUpload; + } + await conn.post_json( + "logs3", + constructLogs3OverflowRequest(overflowUpload.key), + ); + } else { + await conn.post_json("logs3", dataStr); + } + } catch (e) { + error = e; + } + if (error === undefined) { + return; + } + + const isRetrying = i + 1 < this.numTries; + const retryingText = isRetrying ? "" : " Retrying"; + const errorText = (() => { + if (error instanceof FailedHTTPResponse) { + return `${error.status} (${error.text}): ${error.data}`; + } else { + return `${error}`; + } + })(); + const errMsg = `log request failed. Elapsed time: ${ + (now() - startTime) / 1000 + } seconds. Payload size: ${ + payloadBytes + }.${retryingText}\nError: ${errorText}`; + + if (!isRetrying && this.failedPublishPayloadsDir) { + await HTTPBackgroundLogger.writePayloadToDir({ + payloadDir: this.failedPublishPayloadsDir, + payload: dataStr, + }); + this.logFailedPayloadsDir(); + } + + if (!isRetrying) { + debugLogger.warn( + `log request failed after ${this.numTries} retries. Dropping batch`, + ); + throw new Error(errMsg); + } else { + debugLogger.warn(errMsg); + if (isRetrying) { + const sleepTimeS = BACKGROUND_LOGGER_BASE_SLEEP_TIME_S * 2 ** i; + debugLogger.info(`Sleeping for ${sleepTimeS}s`); + await new Promise((resolve) => + setTimeout(resolve, sleepTimeS * 1000), + ); + } + } + } + } + + private registerDroppedItemCount(numItems: number) { + if (numItems <= 0) { + return; + } + this.queueDropLoggingState.numDropped += numItems; + const timeNow = getCurrentUnixTimestamp(); + if ( + timeNow - this.queueDropLoggingState.lastLoggedTimestamp > + this.queueDropLoggingPeriod + ) { + debugLogger.warn( + `Dropped ${this.queueDropLoggingState.numDropped} elements due to full queue`, + ); + if (this.failedPublishPayloadsDir) { + this.logFailedPayloadsDir(); + } + this.queueDropLoggingState.numDropped = 0; + this.queueDropLoggingState.lastLoggedTimestamp = timeNow; + } + } + + private async dumpDroppedEvents( + wrappedItems: LazyValue[], + ) { + const publishPayloadsDir = [ + this.allPublishPayloadsDir, + this.failedPublishPayloadsDir, + ].reduce((acc, x) => (x ? acc.concat([x]) : acc), new Array()); + if (!(wrappedItems.length && publishPayloadsDir.length)) { + return; + } + try { + const [allItems, allAttachments] = + await this.unwrapLazyValues(wrappedItems); + + const dataStr = constructLogs3Data( + allItems.map((x) => stringifyWithOverflowMeta(x)), + ); + const attachmentStr = JSON.stringify( + allAttachments.map((a) => a.debugInfo()), + ); + + const payload = `{"data": ${dataStr}, "attachments": ${attachmentStr}}\n`; + + for (const payloadDir of publishPayloadsDir) { + await HTTPBackgroundLogger.writePayloadToDir({ payloadDir, payload }); + } + } catch (e) { + debugLogger.error(e); + } + } + + private static async writePayloadToDir({ + payloadDir, + payload, + }: { + payloadDir: string; + payload: string; + }) { + if (!(iso.pathJoin && iso.mkdir && iso.writeFile)) { + debugLogger.warn( + "Cannot dump payloads: filesystem-operations not supported on this platform", + ); + return; + } + const payloadFile = iso.pathJoin( + payloadDir, + `payload_${getCurrentUnixTimestamp()}_${uuidv4().slice(0, 8)}.json`, + ); + try { + await iso.mkdir(payloadDir, { recursive: true }); + await iso.writeFile(payloadFile, payload); + } catch (e) { + debugLogger.error( + `Failed to write failed payload to output file ${payloadFile}:\n`, + e, + ); + } + } + + private triggerActiveFlush() { + if (this.activeFlushResolved) { + const flushBoundary = this.lastEnqueuedSequence; + if (flushBoundary <= this.completedSequence) { + return; + } + + this.activeFlushResolved = false; + this.activeFlush = (async () => { + try { + await this.flushThrough(flushBoundary); + } catch (err) { + this.reportFlushError(err); + } finally { + this.activeFlushResolved = true; + if (!this.syncFlush && this.queue.length() > 0) { + this.triggerActiveFlush(); + } + } + })(); + + try { + waitUntil(this.activeFlush); + } catch (error) { + debugLogger.error("Failed to register background flush", error); + } + } + } + + private reportFlushError(error: unknown) { + const errors = error instanceof AggregateError ? error.errors : [error]; + for (const currentError of errors) { + try { + this.onFlushError?.(currentError); + } catch (callbackError) { + debugLogger.error("Error in onFlushError callback", callbackError); + } + } + } + + private logFailedPayloadsDir() { + debugLogger.warn( + `Logging failed payloads to ${this.failedPublishPayloadsDir}`, + ); + } + + // Should only be called by BraintrustState. + public internalReplaceApiConn(apiConn: HTTPConnection): void { + this.apiConn = new LazyValue(async () => apiConn); + } + + public disable(): void { + this._disabled = true; + this.triggerActiveFlush(); + } + + public enforceQueueSizeLimit(enforce: boolean): void { + this.queue.enforceQueueSizeLimit(enforce); + } +} + +declare global { + // Set by the bt eval runner when CLI-controlled BTQL should be pushed down + // into dataset-backed evals. + // eslint-disable-next-line no-var + var __bt_eval_internal_btql: Record | undefined; +} + +// Note: the argument names *must* serialize the same way as the argument names +// for the corresponding python function, because this function may be invoked +// from arguments serialized elsewhere. +async function computeLoggerMetadata( + state: BraintrustState, + { project_name, project_id, project_group_name }: ProjectMetadataArgs, +) { + await state.login({}); + const org_id = state.orgId!; + if (!isEmpty(project_id) && !isEmpty(project_name)) { + return { + org_id, + project: { id: project_id, name: project_name, fullInfo: {} }, + }; + } + + const groupName = project_group_name || undefined; + const cache = state.projectMetadataCache; + const key = JSON.stringify([ + state.appUrl, + org_id, + state.loginToken, + isEmpty(project_id) + ? ["name", project_name || GLOBAL_PROJECT, groupName] + : ["id", project_id], + ]); + const cached = cache.get(key); + if (cached && Date.now() < cached.expiresAt) { + return structuredClone(await cached.promise); + } + + const conn = state.appConn(); + const controller = new AbortController(); + let timeout: ReturnType | undefined; + const entry = { + // The lookup timeout bounds pending requests; the cache TTL starts on success. + expiresAt: Infinity, + promise: Promise.race([ + (async (): Promise => { + if (isEmpty(project_id)) { + const response = await conn.post_json( + "api/project/register", + { + project_name: project_name || GLOBAL_PROJECT, + org_id, + project_group_name: groupName, + }, + controller.signal, + ); + return { + org_id, + project: { + id: response.project.id, + name: response.project.name, + fullInfo: response.project, + }, + }; + } + const response = await conn.get_json( + "api/project", + { id: project_id }, + 0, + controller.signal, + ); + return { + org_id, + project: { + id: project_id, + name: response.name, + fullInfo: response.project, + }, + }; + })(), + new Promise((_, reject) => { + timeout = setTimeout(() => { + const error = new Error( + "Braintrust project lookup timed out after 30 seconds.", + ); + reject(error); + controller.abort(error); + }, PROJECT_LOOKUP_TIMEOUT_MS); + }), + ]), + }; + cache.set(key, entry); + try { + const metadata = await entry.promise; + entry.expiresAt = Date.now() + PROJECT_CACHE_TTL_MS; + return structuredClone(metadata); + } catch (error) { + if (cache.get(key) === entry) { + cache.delete(key); + } + throw error; + } finally { + clearTimeout(timeout); + } +} + +export type InitLoggerOptions = FullLoginOptions & { + /** + * Capture inline media as attachments in instrumentation. Explicit booleans override + * BRAINTRUST_CAPTURE_ATTACHMENTS (1 or true); capture is otherwise disabled. + * With setCurrent: false, this logger and its child spans have an independent policy + * and an omitted option uses the environment, not the global logger's setting. + * Current loggers also update the state's default when this option is supplied; + * omitting it preserves that default. Explicitly logged attachments are unaffected. + */ + captureAttachments?: boolean; + projectName?: string; + projectId?: string; + _createInProjectGroup?: string; + environment?: SpanOriginEnvironment; + setCurrent?: boolean; + state?: BraintrustState; +}; + +/** + * Create a new logger in a specified project. If the project does not exist, it will be created. + * + * @param options Additional options for configuring init(). + * @param options.projectName The name of the project to log into. If unspecified, will default to the Global project. + * @param options.projectId The id of the project to log into. This takes precedence over projectName if specified. + * @param options._createInProjectGroup Experimental: the name of an existing project group to create the project in. Existing projects must already belong to the group, otherwise registration fails with a 409. Membership is re-checked on each fresh project lookup, so moving the project out of the group makes new loggers fail with a 409. Ignored when projectId is provided. + * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. + * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, + * if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. + * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. + * @param options.forceLogin Login again, even if you have already logged in (by default, the logger will not login if you are already logged in) + * @param options.debugLogLevel Enables internal Braintrust SDK troubleshooting output. Use `"error"`, `"warn"`, `"info"`, or `"debug"` to choose an explicit level, or `false` to explicitly disable it. If omitted, the SDK stays silent unless `BRAINTRUST_DEBUG_LOG_LEVEL` is set. + * @param setCurrent If true (the default), set the global current-experiment to the newly-created one. + * @returns The newly created Logger. + */ +export function initLogger(options: Readonly = {}): Logger { + const { + projectName, + projectId, + _createInProjectGroup, + appUrl, + apiKey, + orgName, + forceLogin, + debugLogLevel, + environment, + fetch, + state: stateArg, + } = options || {}; + + const computeMetadataArgs = { + project_name: projectName, + project_id: projectId, + project_group_name: _createInProjectGroup, + }; + + const linkArgs = { + org_name: orgName, + app_url: appUrl, + }; + + const state = stateArg ?? _internalGetGlobalState(); + state._internalSetTraceContextSigningSecret(apiKey); + state.setDebugLogLevel(debugLogLevel); + state.spanOriginEnvironment = detectSpanOriginEnvironment(environment); + + // Enable queue size limit enforcement for initLogger() calls + // This ensures production observability doesn't OOM customer processes + state.enforceQueueSizeLimit(true); + const lazyMetadata: LazyValue = new LazyValue( + async () => { + // Otherwise actually log in. + await state.login({ + orgName, + apiKey, + appUrl, + forceLogin, + fetch, + }); + return computeLoggerMetadata(state, computeMetadataArgs); + }, + ); + + const setCurrent = options.setCurrent ?? true; + if (setCurrent && options.captureAttachments !== undefined) { + state.captureAttachments = options.captureAttachments; + } + const ret = new Logger(state, lazyMetadata, { + captureAttachments: + options.captureAttachments ?? + (setCurrent ? state.captureAttachments : undefined), + computeMetadataArgs, + linkArgs, + }); + if (setCurrent) { + state.currentLogger = ret; + } + return ret; +} + +/** + * Options for logging in to Braintrust. + */ +export interface LoginOptions { + /** + * The URL of the Braintrust App. Defaults to https://www.braintrust.dev. You should not need + * to change this unless you are doing the "Full" deployment. + */ + appUrl?: string; + /** + * The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. + */ + apiKey?: string; + /** + * The name of a specific organization to connect to. Since API keys are scoped to organizations, this parameter is usually + * unnecessary unless you are logging in with a JWT. + */ + orgName?: string; + /** + * A custom fetch implementation to use. + */ + fetch?: typeof globalThis.fetch; + /** + * By default, the SDK installs an event handler that flushes pending writes on the `beforeExit` event. + * If true, this event handler will _not_ be installed. + */ + noExitFlush?: boolean; + /** + * Calls this function if there's an error in the background flusher. + */ + onFlushError?: (error: unknown) => void; + /** + * If true, disables the local span cache used to optimize scorer access + * to trace data. When disabled, scorers will always fetch spans from the + * server. Defaults to false. + */ + disableSpanCache?: boolean; + /** + * Controls internal Braintrust SDK troubleshooting output. + * + * Use `"error"`, `"warn"`, `"info"`, or `"debug"` to control how much + * internal SDK troubleshooting output is emitted. Use `false` to explicitly + * disable this output. + * + * When omitted, the SDK remains silent unless + * `BRAINTRUST_DEBUG_LOG_LEVEL` is set to `"error"`, `"warn"`, `"info"`, or + * `"debug"`. This option only affects local console output; it does not + * change what data is logged to Braintrust. + */ + debugLogLevel?: DebugLogLevel | false; +} + +export type FullLoginOptions = LoginOptions & { + forceLogin?: boolean; +}; + +/** + * Set a global masking function that will be applied to all logged data before sending to Braintrust. + * The masking function will be applied after records are merged but before they are sent to the backend. + * Internally, masking is a state-local export customizer that runs after any + * span customizers and also covers non-span records such as dataset rows and feedback. + * + * @param maskingFunction A function that takes a JSON-serializable object and returns a masked version. + * Set to null to disable masking. + */ +export function setMaskingFunction( + maskingFunction: ((value: unknown) => unknown) | null, +): void { + _internalGetGlobalState().setMaskingFunction(maskingFunction); +} + +/** + * Log into Braintrust. This will prompt you for your API token, which you can find at + * https://www.braintrust.dev/app/token. This method is called automatically by `init()`. + * + * @param options Options for configuring login(). + * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. + * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, + * if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. + * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. + * @param options.forceLogin Login again, even if you have already logged in (by default, this function will exit quickly if you have already logged in) + */ +export async function login( + options: LoginOptions & { forceLogin?: boolean } = {}, +): Promise { + const { forceLogin = false } = options || {}; + + if (!_internalGetGlobalState()) { + _internalSetInitialState(); + } + + const state = _internalGetGlobalState(); + state.setDebugLogLevel(options.debugLogLevel); + if (state.loggedIn && !forceLogin) { + // We have already logged in. If any provided login inputs disagree with our + // existing settings, raise an Exception warning the user to try again with + // `forceLogin: true`. + function checkUpdatedParam( + varname: string, + arg: string | undefined, + orig: string | null, + ) { + if (!isEmpty(arg) && !isEmpty(orig) && arg !== orig) { + throw new Error( + `Re-logging in with different ${varname} (${arg}) than original (${orig}). To force re-login, pass \`forceLogin: true\``, + ); + } + } + checkUpdatedParam("appUrl", options.appUrl, state.appUrl); + checkUpdatedParam( + "apiKey", + options.apiKey + ? HTTPConnection.sanitize_token(options.apiKey) + : undefined, + state.loginToken, + ); + checkUpdatedParam("orgName", options.orgName, state.orgName); + return state; + } + + await state.login(options); + return state; +} + +async function loginToLoaderRequestState({ + appUrl, + apiKey, + orgName, + fetch, +}: Required> & + Pick): Promise { + let orgId: string; + let apiUrl: string; + + if (apiKey === TEST_API_KEY) { + orgId = "test-org-id"; + apiUrl = "https://braintrust.dev/fake-api-url"; + } else { + let loginResponse: Response; + try { + loginResponse = await fetch(_urljoin(appUrl, `/api/apikey/login`), { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + }); + } catch (error) { + throw new HTTPTransportError(error); + } + + const info = await readJSONResponse( + await checkResponse(loginResponse), + true, + ); + const org = selectLoginOrg(info.org_info, orgName); + orgId = org.id; + apiUrl = iso.getEnv("BRAINTRUST_API_URL") ?? org.api_url; + if (!apiUrl) { + throw new Error( + orgName + ? `Unable to log into organization '${orgName}'. Are you sure this credential is scoped to the organization?` + : "Unable to log into any organization with the provided credential.", + ); + } + } + + const apiConnection = new HTTPConnection(apiUrl, fetch, true); + apiConnection.set_token(apiKey); + apiConnection.make_long_lived(); + return { + appUrl, + orgId, + apiConn: () => apiConnection, + }; +} + +export async function loginToState( + options: LoginOptions = {}, +): Promise { + const { + appUrl = iso.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev", + apiKey: apiKeyArg, + orgName = iso.getEnv("BRAINTRUST_ORG_NAME"), + fetch = globalThis.fetch, + } = options || {}; + + const apiKey = + apiKeyArg !== undefined ? apiKeyArg : await iso.getBraintrustApiKey(); + const appPublicUrl = iso.getEnv("BRAINTRUST_APP_PUBLIC_URL") || appUrl; + + const state = new BraintrustState(options); + state.resetLoginInfo(); + + state.appUrl = appUrl; + state.appPublicUrl = appPublicUrl; + + let conn = null; + + if (!apiKey) { + throw new Error( + "Please specify an api key (e.g. by setting BRAINTRUST_API_KEY).", + ); + } else if (apiKey === TEST_API_KEY) { + // This is a weird hook that lets us skip logging in and mocking out the org info. + const testOrgInfo = [ + { + id: "test-org-id", + name: "test-org-name", + api_url: "https://braintrust.dev/fake-api-url", + }, + ]; + state.loggedIn = true; + state.loginToken = TEST_API_KEY; + _saveOrgInfo(state, testOrgInfo, testOrgInfo[0].name); + return state; + } else { + const controller = new AbortController(); + let timeout: ReturnType | undefined; + let info; + try { + // Bound both headers and body reads, including custom fetches that ignore + // cancellation, so a stalled request cannot block subsequent logins. + info = await Promise.race([ + (async () => { + const response = await fetch(_urljoin(appUrl, `/api/apikey/login`), { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + signal: controller.signal, + }); + return readJSONResponse(await checkResponse(response)); + })(), + new Promise((_, reject) => { + timeout = setTimeout(() => { + const error = new Error( + "Braintrust login timed out after 30 seconds.", + ); + reject(error); + controller.abort(error); + }, LOGIN_TIMEOUT_MS); + }), + ]); + } finally { + clearTimeout(timeout); + } + + _saveOrgInfo(state, info.org_info, orgName); + if (!state.apiUrl) { + if (orgName) { + throw new Error( + `Unable to log into organization '${orgName}'. Are you sure this credential is scoped to the organization?`, + ); + } else { + throw new Error( + "Unable to log into any organization with the provided credential.", + ); + } + } + + conn = state.apiConn(); + conn.set_token(apiKey); + + if (!conn) { + throw new Error("Conn should be set at this point (a bug)"); + } + + conn.make_long_lived(); + + // Set the same token in the API + state.appConn().set_token(apiKey); + if (state.proxyUrl) { + state.proxyConn().set_token(apiKey); + } + state.loginToken = conn.token; + state.loggedIn = true; + + // Replace the global logger's apiConn with this one. + state.loginReplaceApiConn(conn); + } + + return state; +} + +// XXX We should remove these global functions now +/** + * Log a single event to the current experiment. The event will be batched and uploaded behind the scenes. + * + * @param event The event to log. See {@link Experiment.log} for full details. + * @returns The `id` of the logged event. + */ +export function log(event: ExperimentLogFullArgs): string { + debugLogger.warn( + "braintrust.log is deprecated and will be removed in a future version of braintrust. Use `experiment.log` instead.", + ); + const e = currentExperiment(); + if (!e) { + throw new Error("Not initialized. Please call init() first"); + } + return e.log(event); +} + +/** + * Summarize the current experiment, including the scores (compared to the closest reference experiment) and metadata. + * + * @param options Options for summarizing the experiment. + * @param options.summarizeScores Whether to summarize the scores. If False, only the metadata will be returned. + * @param options.comparisonExperimentId The experiment to compare against. If None, the most recent experiment on the origin's main branch will be used. + * @returns A summary of the experiment, including the scores (compared to the closest reference experiment) and metadata. + */ +export async function summarize( + options: { + readonly summarizeScores?: boolean; + readonly comparisonExperimentId?: string; + } = {}, +): Promise { + debugLogger.warn( + "braintrust.summarize is deprecated and will be removed in a future version of braintrust. Use `experiment.summarize` instead.", + ); + const e = currentExperiment(); + if (!e) { + throw new Error("Not initialized. Please call init() first"); + } + return await e.summarize(options); +} + +type OptionalStateArg = { + state?: BraintrustState; +}; + +/** + * Returns the currently-active experiment (set by {@link init}). Returns undefined if no current experiment has been set. + */ +export function currentExperiment( + options?: OptionalStateArg, +): Experiment | undefined { + const state = options?.state ?? _internalGetGlobalState(); + return state.currentExperiment; +} + +/** + * Returns the currently-active logger (set by {@link initLogger}). Returns undefined if no current logger has been set. + */ +export function currentLogger(options?: OptionalStateArg): Logger | undefined { + const state = options?.state ?? _internalGetGlobalState(); + return state.currentLogger; +} + +/** + * Return the currently-active span for logging (set by one of the `traced` methods). If there is no active span, returns a no-op span object, which supports the same interface as spans but does no logging. + * + * See {@link Span} for full details. + */ +export function currentSpan(options?: OptionalStateArg): Span { + const state = options?.state ?? _internalGetGlobalState(); + return state.contextManager.getCurrentSpan() ?? NOOP_SPAN; +} + +/** + * Resolve the parent object and any forwarded W3C state in a single pass. + * + * Same precedence as {@link getSpanParentObject}, but also returns the W3C state + * (tracestate + raw traceparent flags) recovered while normalizing the `parent` + * argument, so callers don't have to re-run `normalizeParent` (which would + * re-parse baggage and re-resolve the active logger/experiment, potentially + * disagreeing if state changed between calls). The state is only meaningful when + * a parent slug was resolved; otherwise it is undefined. + */ +function getSpanParentObjectAndPropagatedState( + options?: OptionalStateArg & { parent?: string | PropagationContext }, +): { + parentObject: + | SpanComponentsV3 + | SpanComponentsV4 + | Span + | Experiment + | Logger; + propagatedState: PropagatedState | undefined; +} { + const state = options?.state ?? _internalGetGlobalState(); + + const parentSpan = currentSpan({ state }); + if (!Object.is(parentSpan, NOOP_SPAN)) { + return { parentObject: parentSpan, propagatedState: undefined }; + } + + const parent = options?.parent ?? state.currentParent.getStore(); + const { parentSlug, propagatedState } = normalizeParent(parent, state); + if (parentSlug) { + return { + parentObject: SpanComponentsV4.fromStr(parentSlug), + propagatedState, + }; + } + + const experiment = state.currentExperiment; + if (experiment) { + return { parentObject: experiment, propagatedState: undefined }; + } + const logger = state.currentLogger; + if (logger) { + return { parentObject: logger, propagatedState: undefined }; + } + return { parentObject: NOOP_SPAN, propagatedState: undefined }; +} + +/** + * Mainly for internal use. Return the parent object for starting a span in a global context. + * Applies precedence: current span > propagated parent > experiment > logger. + * + * `parent` may be an exported slug string or an opaque W3C trace-context (from + * {@link extractTraceContextFromHeaders}). + */ +export function getSpanParentObject( + options?: OptionalStateArg & { parent?: string | PropagationContext }, +): SpanComponentsV3 | SpanComponentsV4 | Span | Experiment | Logger { + return getSpanParentObjectAndPropagatedState(options).parentObject; +} + +/** @internal */ +export function _internalExportParentSynchronously( + parent: ReturnType, +): string | undefined { + if ("toStr" in parent) { + return parent.toStr(); + } + + if ("getParentInfo" in parent) { + const parentInfo = parent.getParentInfo(); + if (!parentInfo) { + return undefined; + } + const objectId = parentInfo.objectId.getSync().value; + if (!objectId && !parentInfo.computeObjectMetadataArgs) { + return undefined; + } + return new SpanComponentsV4({ + object_type: parentInfo.objectType, + ...(objectId + ? { object_id: objectId } + : { + compute_object_metadata_args: + parentInfo.computeObjectMetadataArgs ?? {}, + }), + row_id: parent.id, + root_span_id: parent.rootSpanId, + span_id: parent.spanId, + }).toStr(); + } + + const components = braintrustParentToComponents(parent._getOtelParent()); + if (!components) { + return undefined; + } + return new SpanComponentsV4({ + object_type: components.objectType, + ...(components.objectId + ? { object_id: components.objectId } + : { compute_object_metadata_args: components.computeArgs ?? {} }), + }).toStr(); +} + +/** + * Return the Braintrust parent string for the current logger/experiment, if any. + * + * Used as the fallback Braintrust parent on receive, when an inbound request + * carries trace identity (`traceparent`) but no `braintrust.parent` baggage. + */ +function currentBraintrustParent(state?: BraintrustState): string | undefined { + const resolvedState = state ?? _internalGetGlobalState(); + + const experiment = currentExperiment({ state: resolvedState }); + if (experiment) { + try { + return experiment._getOtelParent() ?? undefined; + } catch { + return undefined; + } + } + + const logger = currentLogger({ state: resolvedState }); + if (logger) { + try { + return logger._getOtelParent() ?? undefined; + } catch { + return undefined; + } + } + + return undefined; +} + +interface BraintrustParentComponents { + objectType: SpanObjectTypeV3; + objectId: string | undefined; + computeArgs: Record | undefined; +} + +/** + * Parse a `braintrust.parent` string into object type / id / compute args. + * + * Accepts `project_id:`, `project_name:`, or `experiment_id:`. + * Returns undefined if the value is empty or malformed. + */ +function braintrustParentToComponents( + braintrustParent: string | undefined | null, +): BraintrustParentComponents | undefined { + if (!braintrustParent) { + return undefined; + } + if (braintrustParent.startsWith("project_id:")) { + const objectId = braintrustParent.slice("project_id:".length); + return objectId + ? { + objectType: SpanObjectTypeV3.PROJECT_LOGS, + objectId, + computeArgs: undefined, + } + : undefined; + } + if (braintrustParent.startsWith("project_name:")) { + const name = braintrustParent.slice("project_name:".length); + return name + ? { + objectType: SpanObjectTypeV3.PROJECT_LOGS, + objectId: undefined, + computeArgs: { project_name: name }, + } + : undefined; + } + if (braintrustParent.startsWith("experiment_id:")) { + const objectId = braintrustParent.slice("experiment_id:".length); + return objectId + ? { + objectType: SpanObjectTypeV3.EXPERIMENT, + objectId, + computeArgs: undefined, + } + : undefined; + } + return undefined; +} + +/** + * Set a W3C trace-context header on a carrier, sending the lowercase name. + * + * Per the W3C Trace Context spec (§3.2.1 / §3.3.1), vendors SHOULD send these + * header names in lowercase. `name` is always the canonical lowercase key. A + * plain object carrier is case-sensitive, so any pre-existing case-variant (e.g. + * `Baggage` from a framework that title-cases headers) must be removed first, + * otherwise the carrier would end up with two conflicting headers. + */ +function isMutableHeaderTupleArray( + carrier: TraceContextCarrier, +): carrier is [string, string][] { + return ( + Array.isArray(carrier) && + carrier.every((item) => Array.isArray(item) && typeof item[0] === "string") + ); +} + +function setHeader( + carrier: TraceContextCarrier, + name: string, + value: string, +): void { + if (isMutableHeaderTupleArray(carrier)) { + const lowered = name.toLowerCase(); + for (let i = carrier.length - 1; i >= 0; i--) { + if (carrier[i][0].toLowerCase() === lowered) { + carrier.splice(i, 1); + } + } + carrier.push([name, value]); + return; + } + + const setter = (carrier as { set?: unknown }).set; + if (typeof setter === "function") { + const deleter = (carrier as { delete?: unknown }).delete; + if (typeof deleter === "function") { + try { + deleter.call(carrier, name); + } catch { + // Setting below may still succeed for custom header-like objects. + } + } + setter.call(carrier, name, value); + return; + } + + const nodeSetter = (carrier as { setHeader?: unknown }).setHeader; + if (typeof nodeSetter === "function") { + const remover = (carrier as { removeHeader?: unknown }).removeHeader; + if (typeof remover === "function") { + try { + remover.call(carrier, name); + } catch { + // Setting below may still succeed for custom header-like objects. + } + } + nodeSetter.call(carrier, name, value); + return; + } + + const headerSetter = (carrier as { header?: unknown }).header; + if (typeof headerSetter === "function") { + const deleter = (carrier as { delete?: unknown }).delete; + if (typeof deleter === "function") { + try { + deleter.call(carrier, name); + } catch { + // Setting below may still succeed for custom header-like objects. + } + } + const remover = (carrier as { removeHeader?: unknown }).removeHeader; + if (typeof remover === "function") { + try { + remover.call(carrier, name); + } catch { + // Setting below may still succeed for custom header-like objects. + } + } + headerSetter.call(carrier, name, value); + return; + } + + const headerBag = carrier as { [name: string]: string }; + const lowered = name.toLowerCase(); + for (const key of Object.keys(headerBag)) { + if (key !== name && key.toLowerCase() === lowered) { + delete headerBag[key]; + } + } + headerBag[name] = value; +} + +function deleteHeader(carrier: TraceContextCarrier, name: string): void { + const lowered = name.toLowerCase(); + if (isMutableHeaderTupleArray(carrier)) { + for (let i = carrier.length - 1; i >= 0; i--) { + if (carrier[i][0].toLowerCase() === lowered) { + carrier.splice(i, 1); + } + } + return; + } + + const deleter = (carrier as { delete?: unknown }).delete; + if (typeof deleter === "function") { + try { + deleter.call(carrier, name); + return; + } catch { + // Fall through to other header carrier styles. + } + } + + const remover = (carrier as { removeHeader?: unknown }).removeHeader; + if (typeof remover === "function") { + try { + remover.call(carrier, name); + return; + } catch { + // Fall through to plain object cleanup. + } + } + + const headerBag = carrier as { [name: string]: string }; + for (const key of Object.keys(headerBag)) { + if (key.toLowerCase() === lowered) { + delete headerBag[key]; + } + } +} + +/** + * Inject W3C trace-context headers into a carrier (in place). + * + * Emits `traceparent` from the hex trace/span ids, merges `braintrust.parent` + * into existing `baggage` when known, and forwards any inbound W3C state + * (`tracestate` plus the original `traceparent` trace-flags) carried in + * `propagatedState`. Pre-existing, non-Braintrust baggage entries are preserved. + */ +export function _injectIntoCarrier( + carrier: TraceContextCarrier, + args: { + traceId: string | undefined; + spanId: string | undefined; + braintrustParent: string | undefined | null; + propagatedState?: PropagatedState | undefined; + }, +): void { + // Re-emit the inbound trace-flags verbatim so the upstream sampling decision + // (and any future flag bits) is preserved; defaults to sampled when we + // originated the trace (no inbound flags). + const traceFlags = args.propagatedState?.traceFlags; + const traceparent = traceFlags + ? formatTraceparent(args.traceId, args.spanId, traceFlags) + : formatTraceparent(args.traceId, args.spanId); + if (traceparent === undefined) { + // Ids aren't W3C-shaped (e.g. legacy UUID mode); nothing to propagate. + return; + } + setHeader(carrier, TRACEPARENT_HEADER, traceparent); + + // Forward upstream tracestate (per W3C, only alongside a valid traceparent). + const tracestate = args.propagatedState?.tracestate; + if (tracestate) { + setHeader(carrier, TRACESTATE_HEADER, tracestate); + } + + // Merge braintrust.parent into any existing baggage. Other vendors' members + // are forwarded byte-for-byte (see mergeBaggage) so we never rewrite their + // percent-encoding. + const existing = getHeader(carrier, BAGGAGE_HEADER); + const baggageValue = mergeBaggage(existing, args.braintrustParent); + if (baggageValue !== undefined) { + setHeader(carrier, BAGGAGE_HEADER, baggageValue); + } else if (existing !== undefined) { + // mergeBaggage returns undefined when the existing header only had a stale + // braintrust.parent (or otherwise no members worth forwarding), so remove + // the old header instead of letting a reused carrier propagate bad routing. + // If left in place, a downstream service could attach this trace to the + // previous project/experiment, or leak that parent to an unrelated request. + deleteHeader(carrier, BAGGAGE_HEADER); + } +} + +/** + * Inject W3C trace-context headers for the current (or given) span into a + * carrier. + * + * This is the free-function form of {@link Span.inject}, and the send-side + * counterpart of {@link extractTraceContextFromHeaders}. If no span is + * provided, the currently-active span is used. Propagation is best-effort and + * never throws. + * + * @param carrier Optional carrier (e.g. outbound HTTP headers) to mutate. + * Supports plain objects, Web `Headers`, Node-style `setHeader` carriers, + * framework `header` carriers, and mutable `HeadersInit` tuple arrays. + * @param options.span Optional span to inject. Defaults to the current span. + * @returns The carrier with propagation headers injected. + */ +export function injectTraceContext( + carrier?: undefined, + options?: OptionalStateArg & { span?: Span }, +): Record; + +export function injectTraceContext( + carrier: T, + options?: OptionalStateArg & { span?: Span }, +): T; + +export function injectTraceContext( + carrier?: T, + options?: OptionalStateArg & { span?: Span }, +): T | Record { + const resolvedCarrier = carrier ?? {}; + const span = options?.span ?? currentSpan({ state: options?.state }); + try { + return span.inject(resolvedCarrier); + } catch (e) { + debugLogger.warn(`Error injecting trace context: ${e}`); + return resolvedCarrier; + } +} + +/** + * Extract an opaque W3C trace-context from inbound request headers. + * + * This is the receive-side counterpart of {@link Span.inject} / + * {@link injectTraceContext}. The return value is an opaque propagation context + * that can be passed as `parent` to `startSpan`: + * + * ```ts + * const ctx = extractTraceContextFromHeaders(request.headers); + * traced((span) => { ... }, { name: "handler", parent: ctx }); + * ``` + * + * Only the W3C Trace Context headers are interpreted (`traceparent`, `baggage`, + * `tracestate`); header lookups are case-insensitive. If no valid `traceparent` + * is present, returns undefined (the caller starts a fresh root span). The + * Braintrust container the trace is routed under is resolved when the span is + * created: from the `braintrust.parent` baggage entry, or else the + * currently-active logger/experiment. + * + * Callers should treat the return value as opaque. + * + * @param headers Inbound request headers (e.g. an HTTP framework's headers). + * @returns An opaque context for `startSpan({ parent })`, or undefined. + */ +export function extractTraceContextFromHeaders( + headers: TraceContextHeaders | null | undefined, +): PropagationContext | undefined { + if (!headers) { + return undefined; + } + + const traceparent = getHeader(headers, TRACEPARENT_HEADER); + if (!traceparent || parseTraceparent(traceparent) === undefined) { + return undefined; + } + + const context: PropagationContext = { [TRACEPARENT_HEADER]: traceparent }; + const baggageValue = getHeader(headers, BAGGAGE_HEADER); + if (baggageValue) { + context[BAGGAGE_HEADER] = baggageValue; + } + const tracestate = getHeader(headers, TRACESTATE_HEADER); + if (tracestate) { + // `tracestate` is opaque vendor state that we forward but never author or + // interpret. It is passed along unchanged; receivers that want to use it + // validate it themselves, per the W3C Trace Context spec. + context[TRACESTATE_HEADER] = tracestate; + } + return context; +} + +/** + * Resolve a W3C trace-context into `{ parentSlug, propagatedState }`. + * + * Reads `traceparent` for trace identity and `braintrust.parent` from `baggage` + * (falling back to the currently-active logger/experiment) for routing, and + * builds an internal Braintrust parent slug. Captures the `tracestate` and raw + * `traceparent` flags to forward onward. Returns `{ undefined, undefined }` if + * the context cannot be resolved into a usable parent (so the caller falls back + * to local precedence / a fresh root). + */ +function resolveW3cParent( + context: PropagationContext, + state?: BraintrustState, +): { + parentSlug: string | undefined; + propagatedState: PropagatedState | undefined; +} { + const traceparent = getHeader(context, TRACEPARENT_HEADER); + const parsed = traceparent ? parseTraceparent(traceparent) : undefined; + if (parsed === undefined) { + return { parentSlug: undefined, propagatedState: undefined }; + } + const { traceId, spanId, traceFlags } = parsed; + + // Determine the Braintrust container: baggage -> current logger/experiment. + let braintrustParent: string | undefined = undefined; + const baggageValue = getHeader(context, BAGGAGE_HEADER); + if (baggageValue) { + braintrustParent = parseBaggage(baggageValue)[BRAINTRUST_PARENT_KEY]; + } + if (!braintrustParent) { + braintrustParent = currentBraintrustParent(state); + } + if (!braintrustParent) { + debugLogger.warn( + "Received traceparent without a braintrust.parent and no active logger/experiment; " + + "cannot route the trace. Starting a fresh local span instead.", + ); + return { parentSlug: undefined, propagatedState: undefined }; + } + + const parsedParent = braintrustParentToComponents(braintrustParent); + if (parsedParent === undefined) { + debugLogger.warn( + `Invalid braintrust.parent: ${JSON.stringify(braintrustParent)}`, + ); + return { parentSlug: undefined, propagatedState: undefined }; + } + const { objectType, objectId, computeArgs } = parsedParent; + + // `tracestate` is opaque vendor state, forwarded unchanged and never + // authored or interpreted by Braintrust. + const tracestate = getHeader(context, TRACESTATE_HEADER); + + const slug = new SpanComponentsV4({ + object_type: objectType, + ...(computeArgs + ? { compute_object_metadata_args: computeArgs } + : { object_id: objectId }), + row_id: "bt-propagation", // non-empty to enable span_id/root_span_id + span_id: spanId, + root_span_id: traceId, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + } as SpanComponentsV4Data).toStr(); + return { + parentSlug: slug, + propagatedState: { tracestate, traceFlags, braintrustParent }, + }; +} + +/** + * Normalize a `parent` argument into `{ parentSlug, propagatedState }`. + * + * - object -> interpreted as a W3C trace-context (from + * `extractTraceContextFromHeaders`) + * - string -> an exported span slug (passed through unchanged) + * - undefined -> no parent + * + * `propagatedState` carries any inbound W3C state (tracestate + raw flags) to + * forward on the next `inject`; it is undefined when there is no inbound W3C + * context. + */ +function normalizeParent( + parent: string | PropagationContext | undefined | null, + state?: BraintrustState, +): { + parentSlug: string | undefined; + propagatedState: PropagatedState | undefined; +} { + if (parent && typeof parent === "object") { + return resolveW3cParent(parent, state); + } + return { + parentSlug: parent ?? undefined, + propagatedState: undefined, + }; +} + +/** + * Return true if a parent slug carries span ids the child can link to. + * + * A child links to any parent slug that carries both a span id and a root span + * id, regardless of whether those ids are hex (the default) or legacy UUID. This + * keeps the deprecated `startSpan({ parent: })` path backwards + * compatible: a slug exported by an older (UUID) sender still links into a trace + * on a newer (hex) receiver, and vice versa. The child's own freshly generated + * span id stays in the active format; the backend supports traces whose nodes + * mix id formats across a propagation boundary. Empty ids (no row) are handled + * by the caller. + */ +function parentSpanIdsUsable( + spanId: string | undefined | null, + rootSpanId: string | undefined | null, +): boolean { + return Boolean(spanId) && Boolean(rootSpanId); +} + +export function logError(span: Span, error: unknown): void { + let errorMessage = ""; + let stackTrace = ""; + if (error instanceof Error) { + errorMessage = error.message; + stackTrace = error.stack || ""; + } else { + errorMessage = String(error); + } + span.log({ error: `${errorMessage}\n\n${stackTrace}` }); +} + +/** + * Toplevel function for starting a span. It checks the following (in precedence order): + * * Currently-active span + * * Currently-active experiment + * * Currently-active logger + * + * and creates a span under the first one that is active. Alternatively, if `parent` is specified, it creates a span under the specified parent row. If none of these are active, it returns a no-op span object. + * + * See {@link Span.traced} for full details. + */ +export function traced( + callback: (span: Span) => R, + args?: StartSpanArgs & SetCurrentArg & OptionalStateArg, +): R { + const { setCurrent, ...spanArgs } = args ?? {}; + const span = startSpanImpl(spanArgs); + + return runCatchFinally( + () => { + if (setCurrent ?? true) { + return withCurrent(span, callback); + } else { + return callback(span); + } + }, + (error) => { + logError(span, error); + throw error; + }, + () => span.end(), + ); +} + +type WrapTracedArgs = { + noTraceIO?: boolean; +}; + +/** + * Check if a function is a sync generator function. + * + * Note: This uses Object.prototype.toString which is sufficient for environments that + * support generator functions (ES6+). While our code is compiled to ES2022, consumers + * may run it in various environments. However, if generators aren't supported in their + * environment, the generator functions themselves won't work anyway, making detection moot. + * + * @param fn The function to check. + * @returns True if the function is a sync generator function. + */ +export function isGeneratorFunction(fn: unknown): boolean { + return Object.prototype.toString.call(fn) === "[object GeneratorFunction]"; +} + +/** + * Check if a function is an async generator function. + * + * Note: see isGeneratorFunction disclaimer + * @param fn The function to check. + * @returns True if the function is an async generator function. + */ +export function isAsyncGeneratorFunction(fn: unknown): boolean { + return ( + Object.prototype.toString.call(fn) === "[object AsyncGeneratorFunction]" + ); +} + +/** + * Wrap a sync generator function with tracing. + */ +function wrapTracedSyncGenerator any>( + fn: F, + spanArgs: StartSpanArgs & SetCurrentArg, + noTraceIO: boolean, +): F { + const wrapper = function* ( + this: ThisParameterType, + ...fnArgs: Parameters + ) { + const span = startSpan(spanArgs); + try { + if (!noTraceIO) { + span.log({ input: fnArgs }); + } + + const maxItems = getNumericEnv("BRAINTRUST_MAX_GENERATOR_ITEMS") ?? 1000; + + if (!noTraceIO && maxItems !== 0) { + let collected: unknown[] = []; + let truncated = false; + + const gen = generatorWithCurrent(span, fn.apply(this, fnArgs)); + try { + for (const value of gen) { + if ( + maxItems === -1 || + (!truncated && collected.length < maxItems) + ) { + collected.push(value); + } else { + truncated = true; + collected = []; + debugLogger.warn( + `Generator output exceeded limit of ${maxItems} items, output not logged. ` + + `Increase BRAINTRUST_MAX_GENERATOR_ITEMS or set to -1 to disable limit.`, + ); + } + yield value; + } + + if (!truncated) { + span.log({ output: collected }); + } + } catch (error) { + logError(span, error); + if (!truncated && collected.length > 0) { + span.log({ output: collected }); + } + throw error; + } + } else { + // No output collection + const gen = generatorWithCurrent(span, fn.apply(this, fnArgs)); + for (const value of gen) { + yield value; + } + } + } finally { + span.end(); + } + }; + // Preserve the original function name + Object.defineProperty(wrapper, "name", { value: fn.name }); + return wrapper as F; +} + +/** + * Wrap an async generator function with tracing. + */ +function wrapTracedAsyncGenerator any>( + fn: F, + spanArgs: StartSpanArgs & SetCurrentArg, + noTraceIO: boolean, +): F { + const wrapper = async function* ( + this: ThisParameterType, + ...fnArgs: Parameters + ) { + const span = startSpan(spanArgs); + try { + if (!noTraceIO) { + span.log({ input: fnArgs }); + } + + const maxItems = getNumericEnv("BRAINTRUST_MAX_GENERATOR_ITEMS") ?? 1000; + + if (!noTraceIO && maxItems !== 0) { + let collected: unknown[] = []; + let truncated = false; + + const gen = asyncGeneratorWithCurrent(span, fn.apply(this, fnArgs)); + try { + for await (const value of gen) { + if ( + maxItems === -1 || + (!truncated && collected.length < maxItems) + ) { + collected.push(value); + } else { + truncated = true; + collected = []; + debugLogger.warn( + `Generator output exceeded limit of ${maxItems} items, output not logged. ` + + `Increase BRAINTRUST_MAX_GENERATOR_ITEMS or set to -1 to disable limit.`, + ); + } + yield value; + } + + if (!truncated) { + span.log({ output: collected }); + } + } catch (error) { + logError(span, error); + if (!truncated && collected.length > 0) { + span.log({ output: collected }); + } + throw error; + } + } else { + // No output collection + const gen = asyncGeneratorWithCurrent(span, fn.apply(this, fnArgs)); + for await (const value of gen) { + yield value; + } + } + } finally { + span.end(); + } + }; + // Preserve the original function name + Object.defineProperty(wrapper, "name", { value: fn.name }); + return wrapper as F; +} + +/** + * Wrap a function with `traced`, using the arguments as `input` and return value as `output`. + * Any functions wrapped this way will automatically be traced, similar to the `@traced` decorator + * in Python. If you want to correctly propagate the function's name and define it in one go, then + * you can do so like this: + * + * ```ts + * const myFunc = wrapTraced(async function myFunc(input) { + * const result = await client.chat.completions.create({ + * model: "gpt-3.5-turbo", + * messages: [{ role: "user", content: input }], + * }); + * return result.choices[0].message.content ?? "unknown"; + * }, + * // Optional: if you're using a framework like NextJS that minifies your code, specify the function name and it will be used for the span name + * { name: "myFunc" }, + * ); + * ``` + * Now, any calls to `myFunc` will be traced, and the input and output will be logged automatically. + * If tracing is inactive, i.e. there is no active logger or experiment, it's just a no-op. + * + * @param fn The function to wrap. + * @param args Span-level arguments (e.g. a custom name or type) to pass to `traced`. + * @returns The wrapped function. + */ +export function wrapTraced any>( + fn: F, + args?: StartSpanArgs & SetCurrentArg & WrapTracedArgs, +): F { + const { noTraceIO, ...argsRest } = args ?? {}; + const spanArgs: StartSpanArgs & SetCurrentArg = { + name: fn.name, + type: "function", + ...argsRest, + }; + const hasExplicitInput = spanArgs.event?.input !== undefined; + const hasExplicitOutput = spanArgs.event?.output !== undefined; + + const disableGeneratorTraceIO = + !!noTraceIO || hasExplicitInput || hasExplicitOutput; + // Check if the function is a generator + if (isGeneratorFunction(fn)) { + return wrapTracedSyncGenerator(fn, spanArgs, disableGeneratorTraceIO); + } + + if (isAsyncGeneratorFunction(fn)) { + return wrapTracedAsyncGenerator(fn, spanArgs, disableGeneratorTraceIO); + } + + return ((...fnArgs: Parameters) => + traced((span) => { + if (!noTraceIO && !hasExplicitInput) { + span.log({ input: fnArgs }); + } + + const output = fn(...fnArgs); + + if (!noTraceIO && !hasExplicitOutput) { + if (output instanceof Promise) { + return (async () => { + const result = await output; + span.log({ output: result }); + return result; + })(); + } else { + span.log({ output }); + } + } + + return output; + }, spanArgs)) as F; +} + +/** + * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations + * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", + * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead. + * + * See {@link traced} for full details. + */ +export function startSpan(args?: StartSpanArgs & OptionalStateArg): Span { + return startSpanImpl(args); +} + +export function _internalStartSpan( + args?: InternalStartSpanArgs & OptionalStateArg, +): Span { + return startSpanImpl(args); +} + +/** @internal Start a span whose initial row is merged with concurrent writes. */ +export function _internalStartSpanWithInitialMerge( + args?: InternalStartSpanArgs & OptionalStateArg, +): Span { + return startSpanImpl({ + ...args, + [INITIAL_SPAN_WRITE_AS_MERGE]: true, + } as InternalStartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg); +} + +/** @internal Start a deterministic span under an exported object-only parent. */ +export function _internalStartSpanWithInitialMergeAndParentSpanIds( + args: InternalStartSpanArgs & OptionalStateArg, +): Span { + return startSpanImpl( + { + ...args, + [INITIAL_SPAN_WRITE_AS_MERGE]: true, + } as InternalStartSpanArgs & OptionalStateArg & InitialSpanWriteAsMergeArg, + { useParentSpanIdsForObjectParent: true }, + ); +} + +/** @internal Start a span with SDK-controlled context fields. */ +export function _internalStartSpanWithContext( + args: InternalStartSpanArgs & OptionalStateArg, + context: Record, +): Span { + return startSpanImpl({ + ...args, + [INTERNAL_SPAN_CONTEXT]: context, + }); +} + +/** + * Flush any pending rows to the server. + */ +export async function flush(options?: OptionalStateArg): Promise { + const state = + options?.state ?? + _globalState ?? + ( + globalThis as typeof globalThis & { + [key: symbol]: BraintrustState | undefined; + } + )[Symbol.for("braintrust-state")]; + await state?.bgLogger().flush(); +} + +/** + * Set the fetch implementation to use for requests. You can specify it here, + * or when you call `login`. + * + * @param fetch The fetch implementation to use. + */ +export function setFetch(fetch: typeof globalThis.fetch): void { + _internalGetGlobalState().setFetch(fetch); +} + +function startSpanImpl( + args?: InternalStartSpanArgs & OptionalStateArg & InternalSpanContextArg, + internalOptions?: { useParentSpanIdsForObjectParent?: boolean }, +): Span { + const state = args?.state ?? _internalGetGlobalState(); + + // Resolve the parent object and any forwarded W3C state in one pass, so we + // don't re-normalize `parent` (which could disagree if the active + // logger/experiment changed between calls). + const { parentObject, propagatedState } = + getSpanParentObjectAndPropagatedState(args); + + if ( + parentObject instanceof SpanComponentsV3 || + parentObject instanceof SpanComponentsV4 + ) { + const parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined = + parentObject.data.row_id && + parentSpanIdsUsable( + parentObject.data.span_id, + parentObject.data.root_span_id, + ) + ? { + spanId: parentObject.data.span_id, + rootSpanId: parentObject.data.root_span_id, + } + : internalOptions?.useParentSpanIdsForObjectParent + ? args?.parentSpanIds + : undefined; + // The parent object/state are already resolved from `parent` above; drop + // the raw `parent` so it isn't re-normalized. + const { parent: _ignoredParent, ...spanArgs } = args ?? {}; + return new SpanImpl({ + state, + ...spanArgs, + parentObjectType: parentObject.data.object_type, + parentObjectId: new LazyValue( + spanComponentsToObjectIdLambda(state, parentObject), + ), + parentComputeObjectMetadataArgs: + parentObject.data.compute_object_metadata_args ?? undefined, + parentSpanIds, + propagatedEvent: + args?.propagatedEvent ?? + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + ((parentObject.data.propagated_event ?? undefined) as + | StartSpanEventArgs + | undefined), + propagatedState, + }); + } else { + // The internal entrypoint still accepts exported Braintrust parent slugs. + // Span implementations support that runtime form even though it is no + // longer part of their public method signature. + return parentObject.startSpan(args as StartSpanArgs); + } +} + +/** + * Runs the provided callback with the span as the current span. + */ +export function withCurrent( + span: Span, + callback: (span: Span) => R, + state: BraintrustState | undefined = undefined, +): R { + const currentState = state ?? _internalGetGlobalState(); + return currentState.contextManager.runInContext(span, () => callback(span)); +} + +/** + * Wraps a sync generator to maintain the current span context across yields. + */ +function* generatorWithCurrent( + span: Span, + gen: Generator, + state: BraintrustState | undefined = undefined, +): Generator { + let nextValue: any; + while (true) { + const result = withCurrent( + span, + () => { + try { + return gen.next(nextValue); + } catch (e) { + // Handle generator.throw() + return { value: undefined, done: true, error: e }; + } + }, + state, + ); + + if ("error" in result) { + throw result.error; + } + + if (result.done) { + return result.value; + } + + nextValue = yield result.value; + } +} + +/** + * Wraps an async generator to maintain the current span context across yields. + */ +async function* asyncGeneratorWithCurrent( + span: Span, + gen: AsyncGenerator, + state: BraintrustState | undefined = undefined, +): AsyncGenerator { + let nextValue: any; + while (true) { + const result = await withCurrent( + span, + async () => { + try { + return await gen.next(nextValue); + } catch (e) { + // Handle generator.throw() + return { value: undefined, done: true, error: e }; + } + }, + state, + ); + + if ("error" in result) { + throw result.error; + } + + if (result.done) { + return result.value; + } + + nextValue = yield result.value; + } +} + +export function withParent( + parent: PropagationContext, + callback: () => R, + state: BraintrustState | undefined = undefined, +): R { + return (state ?? _internalGetGlobalState()).currentParent.run(parent, () => + callback(), + ); +} + +export function _internalWithParent( + parent: string | PropagationContext, + callback: () => R, + state: BraintrustState | undefined = undefined, +): R { + return (state ?? _internalGetGlobalState()).currentParent.run(parent, () => + callback(), + ); +} + +function _saveOrgInfo( + state: BraintrustState, + orgInfo: any, + orgName: string | undefined, +) { + const org = selectLoginOrg(orgInfo, orgName); + state.orgId = org.id; + state.orgName = org.name; + state.apiUrl = iso.getEnv("BRAINTRUST_API_URL") ?? org.api_url; + state.proxyUrl = iso.getEnv("BRAINTRUST_PROXY_URL") ?? org.proxy_url; + state.gitMetadataSettings = org.git_metadata || undefined; +} + +function selectLoginOrg(orgInfo: any, orgName: string | undefined) { + if (orgInfo.length === 0) { + throw new LoginInvalidOrgError( + "This user is not part of any organizations.", + ); + } + + for (const org of orgInfo) { + if (orgName === undefined || org.name === orgName) { + return org; + } + } + + throw new LoginInvalidOrgError( + `Organization ${orgName} not found. Must be one of ${orgInfo + .map((org: any) => org.name) + .join(", ")}`, + ); +} + +export function validateTags(tags: readonly string[]): void { + const seen = new Set(); + for (const tag of tags) { + if (typeof tag !== "string") { + throw new Error("tags must be strings"); + } + + if (seen.has(tag)) { + throw new Error(`duplicate tag: ${tag}`); + } + seen.add(tag); + } +} + +function validateAndSanitizeExperimentLogPartialArgs( + event: ExperimentLogPartialArgs, +): SanitizedExperimentLogPartialArgs { + if (event.scores) { + if (Array.isArray(event.scores)) { + throw new Error("scores must be an object, not an array"); + } + for (const [name, rawScore] of Object.entries(event.scores)) { + let score = rawScore; + if (typeof name !== "string") { + throw new Error("score names must be strings"); + } + + if (score === null || score === undefined) { + continue; + } + + if (typeof score === "boolean") { + score = score ? 1 : 0; + event.scores[name] = score; + } + + if (typeof score !== "number") { + throw new Error("score values must be numbers"); + } + if (score < 0 || score > 1) { + throw new Error("score values must be between 0 and 1"); + } + } + } + + if (event.metadata) { + for (const key of Object.keys(event.metadata)) { + if (typeof key !== "string") { + throw new Error("metadata keys must be strings"); + } + } + } + + if (event.metrics) { + for (const [key, value] of Object.entries(event.metrics)) { + if (typeof key !== "string") { + throw new Error("metric keys must be strings"); + } + + if (value !== undefined && typeof value !== "number") { + throw new Error("metric values must be numbers"); + } + } + } + + if ("tags" in event && event.tags) { + validateTags(event.tags); + } + + return { ...event }; +} + +/** + * Creates a deep copy of the given event. Replaces references to user objects + * with placeholder strings to ensure serializability, except for + * {@link Attachment} and {@link ExternalAttachment} objects, which are preserved + * and not deep-copied. + */ +export function deepCopyEvent>( + event: T, +): T { + const attachments: BaseAttachment[] = []; + const ATTACHMENT_INDEX_KEY = "_bt_internal_saved_attachment_idx"; + const ATTACHMENT_MARKER_KEY = "_bt_internal_saved_attachment_marker"; + const attachmentMarker = ++deepCopyEventMarkerCounter; + + // We both check for serializability and round-trip `event` through JSON in + // order to create a "deep copy". This has the benefit of cutting out any + // reference to user objects when the object is logged asynchronously, so that + // in case the objects are modified, the logging is unaffected. In the future, + // this could be changed to a "real" deep copy so that immutable types (long + // strings) do not have to be copied. + const serialized = JSON.stringify(event, (_k, v) => { + if (v instanceof Error) { + return v.message; + } else if (v instanceof SpanImpl || v instanceof NoopSpan) { + return ``; + } else if ( + v && + typeof v === "object" && + v.__braintrust_experiment_marker === true + ) { + return ``; + } else if ( + v && + typeof v === "object" && + v.__braintrust_dataset_marker === true + ) { + return ``; + } else if (v instanceof Logger) { + return ``; + } else if (v instanceof BaseAttachment) { + const idx = attachments.push(v); + return { + [ATTACHMENT_INDEX_KEY]: idx - 1, + [ATTACHMENT_MARKER_KEY]: attachmentMarker, + }; + } else if (v instanceof ReadonlyAttachment) { + return v.reference; + } + return v; + }); + const x = JSON.parse(serialized, (_k, v) => { + if (isDeepCopyAttachmentPlaceholder(v, attachmentMarker)) { + return attachments[v[ATTACHMENT_INDEX_KEY]]; + } + return v; + }); + return x; +} + +let deepCopyEventMarkerCounter = 0; + +function isDeepCopyAttachmentPlaceholder( + value: unknown, + attachmentMarker: number, +): value is Record< + "_bt_internal_saved_attachment_idx" | "_bt_internal_saved_attachment_marker", + number +> { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false; + } + + const record = value as Record; + if ( + !Object.hasOwn(record, "_bt_internal_saved_attachment_idx") || + !Object.hasOwn(record, "_bt_internal_saved_attachment_marker") + ) { + return false; + } + + if (Object.keys(record).length !== 2) { + return false; + } + + const attachmentIndex = record._bt_internal_saved_attachment_idx as number; + const marker = record._bt_internal_saved_attachment_marker; + + return ( + Number.isInteger(attachmentIndex) && + attachmentIndex >= 0 && + marker === attachmentMarker + ); +} + +/** + * Helper function for uploading attachments. Recursively extracts `Attachment` + * and `ExternalAttachment` objects and replaces them with their associated + * `AttachmentReference` objects. + * + * @param event The event to filter. Will be modified in-place. + * @param attachments Flat array of extracted attachments (output parameter). + */ +export function extractAttachments( + event: Record, + attachments: BaseAttachment[], +): void { + for (const [key, value] of Object.entries(event)) { + if (!value) { + continue; + } + + // Base case: Attachment or ExternalAttachment. + if (value instanceof BaseAttachment) { + attachments.push(value); + event[key] = value.reference; + continue; // Attachment cannot be nested. + } + + // Skip if this is already just a reference (no uploader field) + if (value?.type === BRAINTRUST_ATTACHMENT && value.key && !value.uploader) { + // This is already just a reference, skip it + continue; + } + + // Somewhere in our pipeline we're serializing the attachment and then deserializing it. + // This loses the instanceof identity of the attachment, so we need to recreate it because + // the above instance check doesn't catch it. + if (value?.reference?.type === BRAINTRUST_ATTACHMENT && value?.uploader) { + // This looks like a serialized Attachment object, recreate it properly + const attachment = new Attachment({ + data: value.dataDebugString, + filename: value.reference.filename, + contentType: value.reference.content_type, + }); + attachments.push(attachment); + event[key] = attachment.reference; + continue; + } + + // Base case: non-object. + if (!(value instanceof Object)) { + continue; // Nothing to explore recursively. + } + + // Recursive case: object or array. + extractAttachments(value, attachments); + } +} + +/** + * Recursively hydrates any `AttachmentReference` into `Attachment` by modifying + * the input in-place. + * + * @returns The same event instance as the input. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function enrichAttachments>( + event: T, + state: BraintrustState | undefined, +): T { + for (const [key, value] of Object.entries(event)) { + // Base case: AttachmentReference. + const parsedValue = attachmentReferenceSchema.safeParse(value); + if (parsedValue.success) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any + (event as any)[key] = new ReadonlyAttachment(parsedValue.data, state); + continue; + } + + // Base case: non-object. + if (!(value instanceof Object)) { + continue; + } + + // Recursive case: object or array: + enrichAttachments(value, state); + } + + return event; +} + +/** + * Recursively hydrates any `AttachmentReference` into `Attachment` by modifying + * the input in-place. + * + * @returns The same event instance as the input. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function resolveAttachmentsToBase64>( + event: T, + state: BraintrustState | undefined, +): Promise { + for (const [key, value] of Object.entries(event)) { + if (value instanceof ReadonlyAttachment) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any + (event as any)[key] = await value.asBase64Url(); + continue; + } + + // Base case: non-object. + if (!(value instanceof Object)) { + continue; + } + + // Recursive case: object or array: + await resolveAttachmentsToBase64(value, state); + } + + return event; +} + +// Note that this only checks properties that are expected of a complete event. +// validateAndSanitizeExperimentLogPartialArgs should still be invoked (after +// handling special fields like 'id'). +export function validateAndSanitizeExperimentLogFullArgs( + event: ExperimentLogFullArgs, +): ExperimentLogFullArgs { + if (!("input" in event)) { + throw new Error("input must be specified"); + } + + if (isEmpty(event.output)) { + throw new Error("output must be specified"); + } + if (isEmpty(event.scores)) { + throw new Error("scores must be specified"); + } + + return event; +} + +export type BaseMetadata = Record | void; + +export type DefaultMetadataType = void; + +export type EvalCaseOrigin = ObjectReference; + +export type EvalCase = { + input: Input; + tags?: string[]; + // These fields are only set if the EvalCase is part of a Dataset. + id?: string; + _xact_id?: TransactionId; + created?: string | null; + origin?: EvalCaseOrigin; + // This field is used to help re-run a particular experiment row. + upsert_id?: string; + // The number of times to run the evaluator for this specific input. + trialCount?: number; +} & (Expected extends void ? object : { expected: Expected }) & + (Metadata extends void ? object : { metadata: Metadata }); + +let executionCounter = 0; + +export function newId(): string { + return uuidv4(); +} + +interface ResolvedSpanIds { + spanId: string; + rootSpanId: string; + spanParents: string[] | undefined; +} + +/** + * Resolve all span IDs (span_id, root_span_id, span_parents) from explicit values, parent info, or context. + * + * @param spanId - Optional explicit span_id (from public API) + * @param parentSpanIds - Optional explicit parent span IDs (from parent string or parent span) + * @param lookupSpanParent - Whether to look up parent from context manager if no explicit parent. + * - True (default): start_span() inherits parent/root ids from the active span (if it exists) + * - False: don't look up parent in context (e.g. logger.log() doesn't inherit context) + * @param idGenerator - ID generator for creating new span/trace IDs + * @param contextManager - Context manager for looking up parent spans + * @returns ResolvedSpanIds with resolved span_id, root_span_id, and span_parents + */ +function _resolveSpanIds( + spanId: string | undefined, + parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined, + lookupSpanParent: boolean, + idGenerator: IDGenerator, + contextManager: ContextManager, +): ResolvedSpanIds { + // Generate span_id if not provided + const resolvedSpanId = spanId ?? idGenerator.getSpanId(); + + // If we have explicit parent span ids, use them. + if (parentSpanIds) { + return { + spanId: resolvedSpanId, + rootSpanId: parentSpanIds.rootSpanId, + spanParents: + "parentSpanIds" in parentSpanIds + ? parentSpanIds.parentSpanIds + : [parentSpanIds.spanId], + }; + } + + // If we're using the context manager, see if there's an active parent span. + if (lookupSpanParent) { + const parentInfo = contextManager.getParentSpanIds(); + if (parentInfo) { + return { + spanId: resolvedSpanId, + rootSpanId: parentInfo.rootSpanId, + spanParents: parentInfo.spanParents, + }; + } + } + + // No parent - create new root span + // Root span ID behavior differs between UUID and OTEL generators: + // - UUID (legacy): root_span_id === span_id for backwards compatibility + // - OTEL: root_span_id is a separate trace ID, following OpenTelemetry convention + // where trace_id (root_span_id) represents the entire trace, distinct from + // the individual span's ID + let resolvedRootSpanId: string; + if (idGenerator.shareRootSpanId()) { + resolvedRootSpanId = resolvedSpanId; // Backwards compat for UUID mode + } else { + resolvedRootSpanId = idGenerator.getTraceId(); + } + + return { + spanId: resolvedSpanId, + rootSpanId: resolvedRootSpanId, + spanParents: undefined, + }; +} + +/** + * Primary implementation of the `Span` interface. See {@link Span} for full details on each method. + * + * We suggest using one of the various `traced` methods, instead of creating Spans directly. See {@link Span.startSpan} for full details. + */ +export class SpanImpl implements Span { + private _state: BraintrustState; + + private isMerge: boolean; + private loggedEndTime: number | undefined; + private propagatedEvent: StartSpanEventArgs | undefined; + + // For internal use only. + private parentObjectType: SpanObjectTypeV3; + private parentObjectId: LazyValue; + private parentComputeObjectMetadataArgs: Record | undefined; + + private _id: string; + private _spanId: string; + private _rootSpanId: string; + private _spanParents: string[] | undefined; + + // Inbound W3C trace-context state (tracestate + raw traceparent flags) to + // forward on outbound propagation. Captured at the span that received it (via + // extractTraceContextFromHeaders) and inherited by all subspans, so that any + // inject() within the trace re-emits the upstream state unchanged, per the W3C + // Trace Context spec. Not interpreted. + private _propagatedState: PropagatedState | undefined; + + public kind = "span" as const; + + constructor( + args: { + state: BraintrustState; + parentObjectType: SpanObjectTypeV3; + parentObjectId: LazyValue; + parentComputeObjectMetadataArgs: Record | undefined; + parentSpanIds: ParentSpanIds | MultiParentSpanIds | undefined; + defaultRootType?: SpanType; + spanId?: string; + propagatedState?: PropagatedState | undefined; + [CAPTURE_ATTACHMENTS]?: boolean; + } & Omit & + InitialSpanWriteAsMergeArg & + InternalSpanContextArg, + ) { + this._state = args.state; + Object.defineProperty(this, CAPTURE_ATTACHMENTS, { + value: + args[CAPTURE_ATTACHMENTS] ?? + args.state.captureAttachments ?? + captureAttachmentsFromEnv(), + enumerable: true, + configurable: true, + writable: true, + }); + this._propagatedState = args.propagatedState; + const instrumentationName = + getSpanInstrumentationName(args) ?? + INSTRUMENTATION_NAMES.BRAINTRUST_JS_LOGGER; + + const spanAttributes = args.spanAttributes ?? {}; + const rawEvent = args.event ?? {}; + const type = + args.type ?? (args.parentSpanIds ? undefined : args.defaultRootType); + + this.loggedEndTime = undefined; + this.parentObjectType = args.parentObjectType; + this.parentObjectId = args.parentObjectId; + this.parentComputeObjectMetadataArgs = args.parentComputeObjectMetadataArgs; + + // Merge propagatedEvent into event. The propagatedEvent data will get + // propagated-and-merged into every subspan. + this.propagatedEvent = args.propagatedEvent; + if (this.propagatedEvent) { + mergeDicts(rawEvent, this.propagatedEvent); + } + const { id: eventId, ...event } = rawEvent; + + const callerLocation = iso.getCallerLocation(); + const name = (() => { + if (args.name) return args.name; + if (!args.parentSpanIds) return "root"; + if (callerLocation) { + const pathComponents = callerLocation.caller_filename.split("/"); + const filename = pathComponents[pathComponents.length - 1]; + return [callerLocation.caller_functionname] + .concat( + filename ? [`${filename}:${callerLocation.caller_lineno}`] : [], + ) + .join(":"); + } + return "subspan"; + })(); + + const internalData = { + metrics: { + start: args.startTime ?? getCurrentUnixTimestamp(), + }, + context: mergeSpanOriginContext( + { ...callerLocation, ...args[INTERNAL_SPAN_CONTEXT] }, + instrumentationName, + this._state.spanOriginEnvironment, + ), + span_attributes: { + name, + type, + ...spanAttributes, + exec_counter: executionCounter++, + }, + created: new Date().toISOString(), + }; + + this._id = eventId ?? this._state.idGenerator.getSpanId(); + + // Resolve span IDs using the helper function that matches Python's _resolve_span_ids + const resolvedIds = _resolveSpanIds( + args.spanId, + args.parentSpanIds, + true, // Always lookup span parent from context manager unless explicit parent provided + this._state.idGenerator, + this._state.contextManager, + ); + + this._spanId = resolvedIds.spanId; + this._rootSpanId = resolvedIds.rootSpanId; + this._spanParents = resolvedIds.spanParents; + + // Deterministic spans can be initialized concurrently by separate + // workflow executions, so their first write must not replace later merges. + this.isMerge = args[INITIAL_SPAN_WRITE_AS_MERGE] === true; + if (!args[RESUME_SPAN_WITHOUT_INITIAL_WRITE]) { + this.logInternal({ event, internalData }); + } + this.isMerge = true; + } + + public getParentInfo(): { + objectType: SpanObjectTypeV3; + objectId: LazyValue; + computeObjectMetadataArgs: { [x: string]: any } | undefined; + } { + return { + objectType: this.parentObjectType, + objectId: this.parentObjectId, + computeObjectMetadataArgs: this.parentComputeObjectMetadataArgs && { + ...this.parentComputeObjectMetadataArgs, + }, + }; + } + + public get id(): string { + return this._id; + } + + public get spanId(): string { + return this._spanId; + } + + public get rootSpanId(): string { + return this._rootSpanId; + } + + public get spanParents(): string[] { + return this._spanParents ?? []; + } + + public setAttributes(args: Omit): void { + this.logInternal({ internalData: { span_attributes: args } }); + } + + public setSpanParents(parents: string[]): void { + this.logInternal({ internalData: { span_parents: parents } }); + } + + public log(event: ExperimentLogPartialArgs): void { + this.logInternal({ event }); + } + + private logInternal({ + event, + internalData, + }: { + event?: ExperimentLogPartialArgs; + // `internalData` contains fields that are not part of the "user-sanitized" + // set of fields which we want to log in just one of the span rows. + internalData?: Partial; + }): void { + const [serializableInternalData, lazyInternalData] = splitLoggingData({ + event, + internalData, + }); + + // Deep copy mutable user data. + const partialRecord = deepCopyEvent({ + id: this.id, + span_id: this._spanId, + root_span_id: this._rootSpanId, + span_parents: this._spanParents, + ...serializableInternalData, + [IS_MERGE_FIELD]: this.isMerge, + }); + + if (typeof partialRecord.metrics?.end === "number") { + this.loggedEndTime = partialRecord.metrics.end; + } + + // Write to local span cache for scorer access + // Only cache experiment spans - regular logs don't need caching + if (this.parentObjectType === SpanObjectTypeV3.EXPERIMENT) { + const cachedSpan: CachedSpan = { + input: partialRecord.input, + output: partialRecord.output, + expected: partialRecord.expected, + error: partialRecord.error, + scores: partialRecord.scores, + metrics: partialRecord.metrics, + metadata: partialRecord.metadata, + tags: partialRecord.tags, + span_id: this._spanId, + span_parents: this._spanParents, + is_root: !this._spanParents || this._spanParents.length === 0, + span_attributes: partialRecord.span_attributes, + }; + this._state.spanCache.queueWrite( + this._rootSpanId, + this._spanId, + cachedSpan, + ); + } + + const computeRecord = async () => { + const record = { + ...partialRecord, + ...Object.fromEntries( + await Promise.all( + Object.entries(lazyInternalData).map(async ([key, value]) => [ + key, + await value.get(), + ]), + ), + ), + ...new SpanComponentsV3({ + object_type: this.parentObjectType, + object_id: await this.parentObjectId.get(), + }).objectIdFields(), + }; + // Customize inside the memoized lazy value, before attachment processing, + // merging, and masking. Retries reuse the customized record or drop result. + return customizeSpanExport(record) as BackgroundLogEvent | null; + }; + this._state.bgLogger().log([new LazyValue(computeRecord)]); + } + + public logFeedback(event: Omit): void { + logFeedbackImpl(this._state, this.parentObjectType, this.parentObjectId, { + ...event, + id: this.id, + }); + } + + public traced( + callback: (span: Span) => R, + args?: StartSpanArgs & SetCurrentArg, + ): R { + const { setCurrent, ...argsRest } = args ?? {}; + const span = this.startSpan(argsRest); + return runCatchFinally( + () => { + if (setCurrent ?? true) { + return withCurrent(span, callback); + } else { + return callback(span); + } + }, + (error) => { + logError(span, error); + throw error; + }, + () => span.end(), + ); + } + + public startSpan(args?: StartSpanArgs): Span { + const parentSpanIds: ParentSpanIds | undefined = args?.parent + ? undefined + : { spanId: this._spanId, rootSpanId: this._rootSpanId }; + return new SpanImpl({ + state: this._state, + [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], + ...args, + ...startSpanParentArgs({ + state: this._state, + parent: args?.parent, + parentObjectType: this.parentObjectType, + parentObjectId: this.parentObjectId, + parentComputeObjectMetadataArgs: this.parentComputeObjectMetadataArgs, + parentSpanIds, + propagatedEvent: args?.propagatedEvent ?? this.propagatedEvent, + propagatedState: this._propagatedState, + }), + }); + } + + public startSpanWithParents( + spanId: string, + spanParents: string[], + args?: StartSpanArgs, + ): Span { + const parentSpanIds: MultiParentSpanIds = { + parentSpanIds: spanParents, + rootSpanId: this._rootSpanId, + }; + return new SpanImpl({ + state: this._state, + [CAPTURE_ATTACHMENTS]: this[CAPTURE_ATTACHMENTS], + ...args, + ...startSpanParentArgs({ + state: this._state, + parent: args?.parent, + parentObjectType: this.parentObjectType, + parentObjectId: this.parentObjectId, + parentComputeObjectMetadataArgs: this.parentComputeObjectMetadataArgs, + parentSpanIds, + propagatedEvent: args?.propagatedEvent ?? this.propagatedEvent, + propagatedState: this._propagatedState, + }), + spanId, + }); + } + + public end(args?: EndSpanArgs): number { + let endTime: number; + let internalData: Partial = {}; + if (!this.loggedEndTime) { + endTime = args?.endTime ?? getCurrentUnixTimestamp(); + internalData = { metrics: { end: endTime } }; + } else { + endTime = this.loggedEndTime; + } + this.logInternal({ internalData }); + return endTime; + } + + public async export(): Promise { + // Disable span cache since remote function spans won't be in the local cache + this._state.spanCache.disable(); + + return new (getSpanComponentsClass())({ + object_type: this.parentObjectType, + + ...(this.parentComputeObjectMetadataArgs && + !this.parentObjectId.hasSucceeded + ? { compute_object_metadata_args: this.parentComputeObjectMetadataArgs } + : { object_id: await this.parentObjectId.get() }), + row_id: this.id, + span_id: this._spanId, + root_span_id: this._rootSpanId, + propagated_event: this.propagatedEvent, + }).toStr(); + } + + /** + * Return this span's Braintrust parent string (`project_id:`, + * `project_name:`, or `experiment_id:`) for the `braintrust.parent` + * baggage entry, or undefined when it cannot be determined synchronously. + */ + public _getOtelParent(): string | undefined { + if (this.parentObjectType === SpanObjectTypeV3.PROJECT_LOGS) { + const id = + this.parentComputeObjectMetadataArgs?.project_id || + this.parentObjectId.getSync().value; + const name = this.parentComputeObjectMetadataArgs?.project_name; + if (id) { + return `project_id:${id}`; + } else if (name) { + return `project_name:${name}`; + } + } else if (this.parentObjectType === SpanObjectTypeV3.EXPERIMENT) { + const id = + this.parentComputeObjectMetadataArgs?.experiment_id || + this.parentObjectId.getSync().value; + if (id) { + return `experiment_id:${id}`; + } + } + return undefined; + } + + public inject(): Record; + public inject(carrier: T): T; + public inject( + carrier?: T, + ): T | Record { + const resolvedCarrier = carrier ?? {}; + try { + const braintrustParent = + this._getOtelParent() ?? this._propagatedState?.braintrustParent; + if (!braintrustParent) { + // Symmetric with the receive-side warning in resolveW3cParent: + // surface this in the process that caused it, rather than leaving the + // consumer to debug a trace that arrived with no destination. + debugLogger + .forState(this._state) + .warn( + "Injecting trace context without braintrust.parent because the span's " + + "destination is not available yet. The receiver will start a new " + + "local trace instead of continuing this one.", + ); + } + _injectIntoCarrier(resolvedCarrier, { + traceId: this._rootSpanId, + spanId: this._spanId, + braintrustParent, + propagatedState: this._propagatedState, + }); + } catch (e) { + // best-effort: never break the caller + debugLogger.warn(`Error injecting trace context: ${e}`); + } + return resolvedCarrier; + } + + public async permalink(): Promise { + return await permalink(await this.export(), { + state: this._state, + }); + } + + public link(): string { + if (!this.id) { + return NOOP_SPAN_PERMALINK; + } + + try { + let baseUrl: string | null = null; + + if (this.parentObjectType === SpanObjectTypeV3.PROJECT_LOGS) { + // For PROJECT_LOGS, use Logger's _getLinkBaseUrl which checks + // state -> linkArgs -> env var (matches Python) + const curLogger = this._state.currentLogger; + if (curLogger) { + baseUrl = curLogger._getLinkBaseUrl(); + } + } + + // For EXPERIMENT or if Logger not available, fall back to state -> env var + if (!baseUrl) { + baseUrl = _getLinkBaseUrl(this._state); + if (!baseUrl) { + throw new Error("log-in-or-provide-org-name"); + } + } + + return this._link(baseUrl); + } catch (e) { + return getErrPermlink(e instanceof Error ? e.message : String(e)); + } + } + + _link(baseUrl: string): string { + // NOTE[matt]: I believe lazy values should not exist in the span or the logger. + // Nothing in this module should have the possibility of blocking with the lone exception of + // flush() which should be a clear exception. We shouldn't build on it and + // plan to remove it in the future. + const args = this.parentComputeObjectMetadataArgs; + + switch (this.parentObjectType) { + case SpanObjectTypeV3.PROJECT_LOGS: { + // Links to spans require a project id or name. We might not either, so use whatever + // we can to make a link without making a roundtrip to the server. + const projectID = + args?.project_id || this.parentObjectId.getSync().value; + const projectName = args?.project_name; + if (projectID) { + return `${baseUrl}/object?object_type=project_logs&object_id=${projectID}&id=${this._id}`; + } else if (projectName) { + return `${baseUrl}/p/${projectName}/logs?oid=${this._id}`; + } else { + return getErrPermlink("provide-project-name-or-id"); + } + } + case SpanObjectTypeV3.EXPERIMENT: { + // Experiment links require an id, so the sync version will only work after the experiment is + // resolved. + const expID = + args?.experiment_id || this.parentObjectId?.getSync()?.value; + if (!expID) { + return getErrPermlink("provide-experiment-id"); + } else { + return `${baseUrl}/object?object_type=experiment&object_id=${expID}&id=${this._id}`; + } + } + case SpanObjectTypeV3.PLAYGROUND_LOGS: { + // FIXME[matt] I dont believe these are used in the SDK. + return NOOP_SPAN_PERMALINK; + } + default: { + // trigger a compile-time error if we add a new object type + const _exhaustive: never = this.parentObjectType; + // eslint-disable-next-line no-unused-expressions + _exhaustive; + return NOOP_SPAN_PERMALINK; + } + } + } + + async flush(): Promise { + return await this._state.bgLogger().flush(); + } + + public state(): BraintrustState { + return this._state; + } + + // Custom inspect for Node.js console.log + static { + Object.defineProperty(this.prototype, NODE_INSPECT_CUSTOM, { + configurable: true, + writable: true, + value(this: SpanImpl): string { + return `SpanImpl { + kind: '${this.kind}', + id: '${this.id}', + spanId: '${this.spanId}', + rootSpanId: '${this.rootSpanId}', + spanParents: ${JSON.stringify(this.spanParents)} +}`; + }, + }); + } + + // Custom toString + toString(): string { + return `SpanImpl(id=${this.id}, spanId=${this.spanId})`; + } +} + +function splitLoggingData({ + event, + internalData, +}: { + event?: ExperimentLogPartialArgs; + // `internalData` contains fields that are not part of the "user-sanitized" + // set of fields which we want to log in just one of the span rows. + internalData?: Partial; +}): [Partial, Record>] { + // There should be no overlap between the dictionaries being merged, + // except for `sanitized` and `internalData`, where the former overrides + // the latter. + const sanitized = validateAndSanitizeExperimentLogPartialArgs(event ?? {}); + + const sanitizedAndInternalData: Partial & + Partial = {}; + mergeDicts(sanitizedAndInternalData, internalData || {}); + mergeDicts(sanitizedAndInternalData, sanitized); + + const serializableInternalData: typeof sanitizedAndInternalData = {}; + const lazyInternalData: Record> = {}; + + for (const [key, value] of Object.entries(sanitizedAndInternalData) as [ + keyof typeof sanitizedAndInternalData, + any, + ][]) { + if (value instanceof BraintrustStream) { + const streamCopy = value.copy(); + lazyInternalData[key] = new LazyValue(async () => { + return await new Promise((resolve, reject) => { + streamCopy + .toReadableStream() + .pipeThrough(createFinalValuePassThroughStream(resolve, reject)) + .pipeTo(devNullWritableStream()); + }); + }); + } else if (value instanceof ReadableStream) { + lazyInternalData[key] = new LazyValue(async () => { + return await new Promise((resolve, reject) => { + value + .pipeThrough(createFinalValuePassThroughStream(resolve, reject)) + .pipeTo(devNullWritableStream()); + }); + }); + } else { + serializableInternalData[key] = value; + } + } + + return [serializableInternalData, lazyInternalData]; +} + +// Interface declarations preserve symbol methods with Oxc isolated emit. +export interface NoopSpan { + [NODE_INSPECT_CUSTOM](): string; +} + +export interface BraintrustState { + [RESET_CONTEXT_MANAGER_STATE](): void; + [NODE_INSPECT_CUSTOM](): string; +} + +export interface HTTPConnection { + [NODE_INSPECT_CUSTOM](): string; +} + +export interface SpanImpl { + [NODE_INSPECT_CUSTOM](): string; + /** @internal Snapshot inherited by descendants and delayed instrumentation. */ + readonly [CAPTURE_ATTACHMENTS]: boolean; +} + +export interface Logger { + /** @internal */ + readonly [CAPTURE_ATTACHMENTS]: boolean; +} diff --git a/js/src/logger/datasets.ts b/js/src/logger/datasets.ts new file mode 100644 index 000000000..8a97c077b --- /dev/null +++ b/js/src/logger/datasets.ts @@ -0,0 +1,1022 @@ +/// +import { v4 as uuidv4 } from "uuid"; +import { + AnyDatasetRecord, + BackgroundLogEvent, + DatasetRecord, + ensureNewDatasetRecord, + IS_MERGE_FIELD, + isObject, +} from "../util"; +import { DatasetSnapshot as datasetSnapshotSchema } from "../sdk-schemas"; +import type { DatasetSnapshot, ObjectReference } from "../sdk-types"; +import { z } from "zod/v3"; +import { LazyValue } from "../util"; +import { + FullLoginOptions, + BraintrustState, + ProjectDatasetMetadata, + _internalGetGlobalState, + enrichAttachments, + ObjectMetadata, + validateTags, + deepCopyEvent, +} from "./core"; +import { ObjectFetcher, WithTransactionId } from "./objects"; + +const datasetSnapshotRegisterResponseSchema = z.object({ + dataset_snapshot: datasetSnapshotSchema, + found_existing: z.boolean().optional(), +}); + +const datasetObjectInfoSchema = z.object({ + object_id: z.string(), + object_name: z.string(), + parent_cols: z.object({ + project: z.object({ + id: z.string(), + name: z.string(), + }), + }), +}); + +const datasetRestorePreviewResultSchema = z.object({ + rows_to_restore: z.number(), + rows_to_delete: z.number(), +}); + +export type DatasetRestorePreviewResult = { + rows_to_restore: number; + rows_to_delete: number; +}; + +const datasetRestoreResultSchema = z.object({ + xact_id: z.string().nullable(), + rows_restored: z.number(), + rows_deleted: z.number(), +}); + +export type DatasetRestoreResult = { + xact_id: string | null; + rows_restored: number; + rows_deleted: number; +}; + +/** + * Reference to a dataset by ID and optional explicit selector. + */ +export type DatasetSelection = { + version?: string; + environment?: string; + snapshotName?: string; +}; + +type DatasetSnapshotNameLookup = { + snapshotName: string; + xactId?: never; +}; + +type DatasetSnapshotXactLookup = { + snapshotName?: never; + xactId: string; +}; + +export type DatasetSnapshotLookup = + | DatasetSnapshotNameLookup + | DatasetSnapshotXactLookup; + +function isDatasetSnapshotNameLookup( + lookup: DatasetSnapshotLookup, +): lookup is DatasetSnapshotNameLookup { + return "snapshotName" in lookup; +} + +function assertDatasetSnapshotLookup(lookup: { + snapshotName?: string; + xactId?: string; +}): asserts lookup is DatasetSnapshotLookup { + const hasSnapshotName = lookup.snapshotName !== undefined; + const hasXactId = lookup.xactId !== undefined; + if (hasSnapshotName === hasXactId) { + throw new Error("Exactly one of snapshotName or xactId must be provided"); + } +} + +type DatasetPinState = { + lazyPinnedVersion?: LazyValue; + pinnedEnvironment?: string; + pinnedSnapshotName?: string; +}; + +export type DatasetRef = { + id: string; +} & DatasetSelection; + +export type InitDatasetOptions = FullLoginOptions & { + dataset?: string; + datasetId?: string; + description?: string; + version?: string; + environment?: string; + snapshotName?: string; + projectId?: string; + metadata?: Record; + state?: BraintrustState; + _internal_btql?: Record; +}; + +export type FullInitDatasetOptions = { + project?: string; +} & InitDatasetOptions; + +async function getDatasetSnapshots( + params: + | { + state: BraintrustState; + datasetId: string; + } + | ({ + state: BraintrustState; + datasetId: string; + } & DatasetSnapshotLookup), +): Promise { + const { state, datasetId } = params; + return datasetSnapshotSchema.array().parse( + await state.appConn().post_json("api/dataset_snapshot/get", { + dataset_id: datasetId, + ...("snapshotName" in params ? { name: params.snapshotName } : {}), + ...("xactId" in params ? { xact_id: params.xactId } : {}), + }), + ); +} + +async function getDatasetSnapshot( + params: { + state: BraintrustState; + datasetId: string; + } & DatasetSnapshotLookup, +): Promise { + assertDatasetSnapshotLookup(params); + const snapshots = await getDatasetSnapshots(params); + if (snapshots.length > 1) { + throw new Error( + isDatasetSnapshotNameLookup(params) + ? `Expected a unique dataset snapshot named "${params.snapshotName}" for ${params.datasetId}` + : `Expected a unique dataset snapshot for xact_id "${params.xactId}" in ${params.datasetId}`, + ); + } + return snapshots[0]; +} + +function normalizeDatasetSelection({ + version, + environment, + snapshotName, +}: DatasetSelection): DatasetSelection { + if (version !== undefined) { + return { version }; + } + + if (snapshotName !== undefined) { + return { snapshotName }; + } + + if (environment !== undefined) { + return { environment }; + } + + return {}; +} + +async function resolveDatasetSnapshotName({ + state, + datasetId, + snapshotName, +}: { + state: BraintrustState; + datasetId: string; + snapshotName: string; +}): Promise { + const match = await getDatasetSnapshot({ + state, + datasetId, + snapshotName, + }); + if (match === undefined) { + throw new Error( + `Dataset snapshot "${snapshotName}" not found for ${datasetId}`, + ); + } + return match.xact_id; +} + +async function resolveDatasetSnapshotNameForMetadata({ + state, + lazyMetadata, + snapshotName, +}: { + state: BraintrustState; + lazyMetadata: LazyValue; + snapshotName: string; +}): Promise { + const metadata = await lazyMetadata.get(); + return await resolveDatasetSnapshotName({ + state, + datasetId: metadata.dataset.id, + snapshotName, + }); +} + +async function resolveDatasetEnvironment({ + state, + datasetId, + environment, +}: { + state: BraintrustState; + datasetId: string; + environment: string; +}): Promise { + const environmentObjectPath = `environment-object/dataset/${datasetId}/${encodeURIComponent(environment)}`; + const response = + state.orgName == null + ? await state.apiConn().get_json(environmentObjectPath) + : await state.apiConn().get_json(environmentObjectPath, { + org_name: state.orgName, + }); + return z.object({ object_version: z.string() }).parse(response) + .object_version; +} + +async function resolveDatasetEnvironmentForMetadata({ + state, + lazyMetadata, + environment, +}: { + state: BraintrustState; + lazyMetadata: LazyValue; + environment: string; +}): Promise { + const metadata = await lazyMetadata.get(); + return await resolveDatasetEnvironment({ + state, + datasetId: metadata.dataset.id, + environment, + }); +} + +export async function serializeDatasetForExperiment({ + dataset, + state, +}: { + dataset: AnyDataset | DatasetRef; + state: BraintrustState; +}): Promise<{ datasetId: string; datasetVersion?: string }> { + if (!Dataset.isDataset(dataset)) { + const selection = normalizeDatasetSelection(dataset); + + if (selection.version !== undefined) { + return { + datasetId: dataset.id, + datasetVersion: selection.version, + }; + } + + if (selection.snapshotName !== undefined) { + return { + datasetId: dataset.id, + datasetVersion: await resolveDatasetSnapshotName({ + state, + datasetId: dataset.id, + snapshotName: selection.snapshotName, + }), + }; + } + + if (selection.environment !== undefined) { + return { + datasetId: dataset.id, + datasetVersion: await resolveDatasetEnvironment({ + state, + datasetId: dataset.id, + environment: selection.environment, + }), + }; + } + + return { + datasetId: dataset.id, + }; + } + + const evalData = await dataset.toEvalData(); + const selection = normalizeDatasetSelection({ + version: evalData.dataset_version, + environment: evalData.dataset_environment, + snapshotName: evalData.dataset_snapshot_name, + }); + + if (selection.version !== undefined) { + return { + datasetId: evalData.dataset_id, + datasetVersion: selection.version, + }; + } + + const datasetVersion = await dataset.version(); + if (datasetVersion !== undefined) { + return { + datasetId: evalData.dataset_id, + datasetVersion, + }; + } + + return { + datasetId: evalData.dataset_id, + }; +} + +/** + * Initialize a dataset by name or ID. When initializing by name, the dataset and its project will be created if they do not exist. + * + * @param options Options for configuring initDataset(). + * @param options.project The name of the project to create the dataset in. Must specify at least one of `project` or `projectId` unless `datasetId` is provided. + * @param options.dataset The name of the dataset to create. If not specified, a name will be generated automatically. Ignored if `datasetId` is provided. + * @param options.datasetId The ID of an existing dataset. Takes precedence over `dataset` and does not require `project` or `projectId`. + * @param options.description An optional description when initializing a dataset by name. Cannot be used with `datasetId`. + * @param options.version Pin the dataset to a specific version xact_id. If `snapshotName` or `environment` are also provided, `version` takes precedence. + * @param options.snapshotName Pin the dataset to the version captured by this named snapshot. If `environment` is also provided, `snapshotName` takes precedence. + * @param options.environment Pin the dataset to the version tagged with this environment slug. + * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. + * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. + * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. + * @param options.projectId The id of the project to create the dataset in. This takes precedence over `project` if specified and is not required when `datasetId` is provided. + * @param options.metadata A dictionary with additional data when initializing a dataset by name. Cannot be used with `datasetId`. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. + * @returns The initialized Dataset. + */ +export function initDataset( + options: Readonly, +): Dataset { + const { + project, + dataset, + datasetId, + description, + version, + snapshotName, + environment, + appUrl, + apiKey, + orgName, + fetch, + forceLogin, + projectId, + metadata, + state: stateArg, + _internal_btql, + } = options; + if ( + datasetId !== undefined && + (description !== undefined || metadata !== undefined) + ) { + throw new Error( + "Cannot specify description or metadata when datasetId is provided", + ); + } + + const selection = normalizeDatasetSelection({ + version, + environment, + snapshotName, + }); + const normalizedVersion = selection.version; + const normalizedEnvironment = selection.environment; + const normalizedSnapshotName = selection.snapshotName; + + const cliInternalBtql = globalThis.__bt_eval_internal_btql; + const internalBtql = + cliInternalBtql === undefined + ? _internal_btql + : _internal_btql === undefined + ? cliInternalBtql + : isObject(_internal_btql) + ? { ...cliInternalBtql, ..._internal_btql } + : _internal_btql; + + const state = stateArg ?? _internalGetGlobalState(); + + const lazyMetadata: LazyValue = new LazyValue( + async () => { + await state.login({ + orgName, + apiKey, + appUrl, + fetch, + forceLogin, + }); + + if (datasetId !== undefined) { + const objectInfo = datasetObjectInfoSchema.array().parse( + await state.appConn().post_json("api/self/get_object_info", { + object_type: "dataset", + object_ids: [datasetId], + }), + ); + if (objectInfo.length === 0) { + throw new Error(`Dataset with ID ${datasetId} not found`); + } + if (objectInfo.length !== 1) { + throw new Error( + `Expected exactly one dataset with ID ${datasetId}, but found ${objectInfo.length}`, + ); + } + + const datasetInfo = objectInfo[0]; + return { + project: { + id: datasetInfo.parent_cols.project.id, + name: datasetInfo.parent_cols.project.name, + fullInfo: datasetInfo.parent_cols.project, + }, + dataset: { + id: datasetInfo.object_id, + name: datasetInfo.object_name, + fullInfo: datasetInfo, + }, + }; + } + + const args: Record = { + org_id: state.orgId, + project_name: project, + project_id: projectId, + dataset_name: dataset, + description, + metadata, + }; + const response = await state + .appConn() + .post_json("api/dataset/register", args); + + return { + project: { + id: response.project.id, + name: response.project.name, + fullInfo: response.project, + }, + dataset: { + id: response.dataset.id, + name: response.dataset.name, + fullInfo: response.dataset, + }, + }; + }, + ); + + const resolvedVersion = + normalizedVersion !== undefined + ? normalizedVersion + : normalizedSnapshotName !== undefined + ? new LazyValue(async () => { + return await resolveDatasetSnapshotNameForMetadata({ + state, + lazyMetadata, + snapshotName: normalizedSnapshotName, + }); + }) + : normalizedEnvironment !== undefined + ? new LazyValue(async () => { + return await resolveDatasetEnvironmentForMetadata({ + state, + lazyMetadata, + environment: normalizedEnvironment, + }); + }) + : undefined; + + const datasetObject = new Dataset( + stateArg ?? _internalGetGlobalState(), + lazyMetadata, + typeof resolvedVersion === "string" ? resolvedVersion : undefined, + internalBtql, + resolvedVersion instanceof LazyValue || + normalizedEnvironment !== undefined || + normalizedSnapshotName !== undefined + ? { + ...(resolvedVersion instanceof LazyValue + ? { + lazyPinnedVersion: resolvedVersion, + } + : {}), + ...(normalizedEnvironment !== undefined + ? { + pinnedEnvironment: normalizedEnvironment, + } + : {}), + ...(normalizedSnapshotName !== undefined + ? { + pinnedSnapshotName: normalizedSnapshotName, + } + : {}), + } + : undefined, + ); + + return datasetObject; +} + +/** + * A dataset is a collection of records, such as model inputs and expected outputs, which represent + * data you can use to evaluate and fine-tune models. You can log production data to datasets, + * curate them with interesting examples, edit/delete records, and run evaluations against them. + * + * You should not create `Dataset` objects directly. Instead, use the `braintrust.initDataset()` method. + */ +export class Dataset extends ObjectFetcher { + private readonly lazyMetadata: LazyValue; + private readonly __braintrust_dataset_marker = true; + private newRecords = 0; + private lazyPinnedVersion?: LazyValue; + private pinnedEnvironment?: string; + private pinnedSnapshotName?: string; + + constructor( + private state: BraintrustState, + lazyMetadata: LazyValue, + pinnedVersion?: string, + _internal_btql?: Record, + pinState?: DatasetPinState, + ) { + super( + "dataset", + pinnedVersion, + (r: AnyDatasetRecord) => + ensureNewDatasetRecord( + enrichAttachments(r, this.state), + ) as WithTransactionId, + _internal_btql, + ); + void this.__braintrust_dataset_marker; + this.lazyMetadata = lazyMetadata; + this.lazyPinnedVersion = pinState?.lazyPinnedVersion; + this.pinnedEnvironment = pinState?.pinnedEnvironment; + this.pinnedSnapshotName = pinState?.pinnedSnapshotName; + } + + public get id(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).dataset.id; + })(); + } + + public get name(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).dataset.name; + })(); + } + + public get project(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).project; + })(); + } + + public get loggingState(): BraintrustState { + return this.state; + } + + public async toEvalData(): Promise<{ + dataset_id: string; + dataset_version?: string; + dataset_environment?: string; + dataset_snapshot_name?: string; + _internal_btql?: Record; + }> { + await this.getState(); + const metadata = await this.lazyMetadata.get(); + const pinnedVersion = this.getPinnedVersion(); + const internalBtql = this.getInternalBtql(); + + return { + dataset_id: metadata.dataset.id, + ...(this.pinnedEnvironment !== undefined + ? { + dataset_environment: this.pinnedEnvironment, + } + : {}), + ...(this.pinnedEnvironment === undefined && + this.pinnedSnapshotName !== undefined + ? { + dataset_snapshot_name: this.pinnedSnapshotName, + } + : {}), + ...(this.pinnedEnvironment === undefined && + this.pinnedSnapshotName === undefined && + pinnedVersion !== undefined + ? { + dataset_version: pinnedVersion, + } + : {}), + ...(internalBtql !== undefined ? { _internal_btql: internalBtql } : {}), + }; + } + + protected async getState(): Promise { + // Ensure the login state is populated by awaiting lazyMetadata. + await this.lazyMetadata.get(); + if ( + this.lazyPinnedVersion !== undefined && + this.getPinnedVersion() === undefined + ) { + this.setPinnedVersion(await this.lazyPinnedVersion.get()); + } + return this.state; + } + + public override async version(options?: { + batchSize?: number; + }): Promise { + const pinnedVersion = this.getPinnedVersion(); + if (pinnedVersion !== undefined) { + return pinnedVersion; + } + await this.getState(); + return await super.version(options); + } + + private validateEvent({ + metadata, + tags, + }: { + metadata?: Record; + tags?: string[]; + }) { + if (metadata !== undefined) { + for (const key of Object.keys(metadata)) { + if (typeof key !== "string") { + throw new Error("metadata keys must be strings"); + } + } + } + + if (tags) { + validateTags(tags); + } + } + + private createArgs({ + id, + input, + expected, + metadata, + tags, + origin, + isMerge, + }: { + id: string; + input?: unknown; + expected?: unknown; + metadata?: Record; + tags?: string[]; + origin?: ObjectReference; + isMerge?: boolean; + }): LazyValue { + return new LazyValue(async () => { + const dataset_id = await this.id; + const args: BackgroundLogEvent = { + id, + input, + expected, + tags, + dataset_id, + created: !isMerge ? new Date().toISOString() : undefined, //if we're merging/updating an event we will not add this ts + metadata, + origin, + ...(!!isMerge + ? { + [IS_MERGE_FIELD]: true, + } + : {}), + }; + + return args; + }); + } + + /** + * Insert a single record to the dataset. The record will be batched and uploaded behind the scenes. If you pass in an `id`, + * and a record with that `id` already exists, it will be overwritten (upsert). + * + * @param event The event to log. + * @param event.input The argument that uniquely define an input case (an arbitrary, JSON serializable object). + * @param event.expected The output of your application, including post-processing (an arbitrary, JSON serializable object). + * @param event.tags (Optional) a list of strings that you can use to filter and group records later. + * @param event.metadata (Optional) a dictionary with additional data about the test example, model outputs, or just + * about anything else that's relevant, that you can use to help find and analyze examples later. For example, you could log the + * `prompt`, example's `id`, or anything else that would be useful to slice/dice later. The values in `metadata` can be any + * JSON-serializable type, but its keys must be strings. + * @param event.origin (Optional) a reference to the source object this dataset record was derived from. + * @param event.id (Optional) a unique identifier for the event. If you don't provide one, Braintrust will generate one for you. + * @returns The `id` of the logged record. + */ + public insert({ + input, + expected, + metadata, + tags, + id, + origin, + }: { + readonly input?: unknown; + readonly expected?: unknown; + readonly tags?: string[]; + readonly metadata?: Record; + readonly id?: string; + readonly origin?: ObjectReference; + }): string { + this.validateEvent({ metadata, tags }); + + const rowId = id || uuidv4(); + const args = this.createArgs( + deepCopyEvent({ + id: rowId, + input, + expected, + metadata, + tags, + origin, + isMerge: false, + }), + ); + + this.state.bgLogger().log([args]); + this.newRecords++; + return rowId; + } + + /** + * Update fields of a single record in the dataset. The updated fields will be batched and uploaded behind the scenes. + * You must pass in an `id` of the record to update. Only the fields provided will be updated; other fields will remain unchanged. + * + * @param event The fields to update in the record. + * @param event.id The unique identifier of the record to update. + * @param event.input (Optional) The new input value for the record (an arbitrary, JSON serializable object). + * @param event.expected (Optional) The new expected output value for the record (an arbitrary, JSON serializable object). + * @param event.tags (Optional) A list of strings to update the tags of the record. + * @param event.metadata (Optional) A dictionary to update the metadata of the record. The values in `metadata` can be any + * JSON-serializable type, but its keys must be strings. + * @returns The `id` of the updated record. + */ + public update({ + input, + expected, + metadata, + tags, + id, + }: { + readonly id: string; + readonly input?: unknown; + readonly expected?: unknown; + readonly tags?: string[]; + readonly metadata?: Record; + }): string { + this.validateEvent({ metadata, tags }); + + const args = this.createArgs( + deepCopyEvent({ + id, + input, + expected, + metadata, + tags, + isMerge: true, + }), + ); + + this.state.bgLogger().log([args]); + return id; + } + + public delete(id: string): string { + const args = new LazyValue(async () => ({ + id, + dataset_id: await this.id, + created: new Date().toISOString(), + _object_delete: true, + })); + + this.state.bgLogger().log([args]); + return id; + } + + public async createSnapshot({ + name, + description, + update, + }: { + readonly name: string; + readonly description?: string; + readonly update?: boolean; + }): Promise { + await this.flush(); + const state = await this.getState(); + const datasetId = await this.id; + // Live-row versions omit deletions. After flushing, get a server transaction + // boundary that includes every completed write, even if no live rows remain. + // getState() above also resolves versions pinned by snapshot or environment. + const currentVersion = + this.getPinnedVersion() ?? + (await (await state.apiConn().get("xact-id")).text()); + const response = await state + .appConn() + .post_json("api/dataset_snapshot/register", { + dataset_id: datasetId, + dataset_snapshot_name: name, + description, + xact_id: currentVersion, + update, + }); + return datasetSnapshotRegisterResponseSchema.parse(response) + .dataset_snapshot; + } + + public async listSnapshots(): Promise { + const state = await this.getState(); + return await getDatasetSnapshots({ + state, + datasetId: await this.id, + }); + } + + public async getSnapshot( + lookup: DatasetSnapshotLookup, + ): Promise { + const state = await this.getState(); + const datasetId = await this.id; + return await getDatasetSnapshot({ + state, + datasetId, + ...lookup, + }); + } + + public async updateSnapshot( + snapshotId: string, + { + name, + description, + }: { + readonly name?: string; + readonly description?: string | null; + }, + ): Promise { + const state = await this.getState(); + return datasetSnapshotSchema.parse( + await state.appConn().post_json("api/dataset_snapshot/patch_id", { + id: snapshotId, + name, + description, + }), + ); + } + + public async deleteSnapshot(snapshotId: string): Promise { + const state = await this.getState(); + return datasetSnapshotSchema.parse( + await state.appConn().post_json("api/dataset_snapshot/delete_id", { + id: snapshotId, + }), + ); + } + + public async restorePreview({ + version, + }: { + readonly version: string; + }): Promise { + await this.flush(); + const state = await this.getState(); + const datasetId = await this.id; + return datasetRestorePreviewResultSchema.parse( + await state + .apiConn() + .post_json(`v1/dataset/${datasetId}/restore/preview`, { + version, + }), + ); + } + + public async restore({ + version, + }: { + readonly version: string; + }): Promise { + await this.flush(); + const state = await this.getState(); + const datasetId = await this.id; + return datasetRestoreResultSchema.parse( + await state.apiConn().post_json(`v1/dataset/${datasetId}/restore`, { + version, + }), + ); + } + + /** + * Summarize the dataset, including high level metrics about its size and other metadata. + * @param summarizeData Whether to summarize the data. If false, only the metadata will be returned. + * @returns `DatasetSummary` + * @returns A summary of the dataset. + */ + public async summarize( + options: { readonly summarizeData?: boolean } = {}, + ): Promise { + const { summarizeData = true } = options || {}; + + await this.flush(); + const state = await this.getState(); + const projectUrl = `${state.appPublicUrl}/app/${encodeURIComponent( + state.orgName!, + )}/p/${encodeURIComponent((await this.project).name)}`; + const datasetUrl = `${projectUrl}/datasets/${encodeURIComponent( + await this.name, + )}`; + + let dataSummary: DataSummary | undefined; + if (summarizeData) { + const rawDataSummary = z + .object({ + total_records: z.number(), + }) + .parse( + await state.apiConn().get_json( + "dataset-summary", + { + dataset_id: await this.id, + }, + 3, + ), + ); + dataSummary = { + newRecords: this.newRecords, + totalRecords: rawDataSummary.total_records, + }; + } + + return { + projectName: (await this.project).name, + datasetName: await this.name, + projectUrl, + datasetUrl, + dataSummary, + }; + } + + /** + * Flush any pending rows to the server. + */ + async flush(): Promise { + return await this.state.bgLogger().flush(); + } + + public static isDataset(data: unknown): data is Dataset { + return ( + typeof data === "object" && + data !== null && + "__braintrust_dataset_marker" in data + ); + } +} + +export type AnyDataset = Dataset; + +/** + * Summary of a dataset's data. + * + * @property totalRecords Total records in the dataset. + */ +export interface DataSummary { + /** + * New or updated records added in this session. + */ + newRecords: number; + /** + * Total records in the dataset. + */ + totalRecords: number; +} + +/** + * Summary of a dataset's scores and metadata. + * + * @property projectName Name of the project that the dataset belongs to. + * @property datasetName Name of the dataset. + * @property projectUrl URL to the project's page in the Braintrust app. + * @property datasetUrl URL to the experiment's page in the Braintrust app. + * @property dataSummary Summary of the dataset's data. + */ +export interface DatasetSummary { + projectName: string; + datasetName: string; + projectUrl: string; + datasetUrl: string; + dataSummary: DataSummary | undefined; +} diff --git a/js/src/logger/experiments.ts b/js/src/logger/experiments.ts new file mode 100644 index 000000000..14dd4c083 --- /dev/null +++ b/js/src/logger/experiments.ts @@ -0,0 +1,849 @@ +/// +import { debugLogger } from "../debug-logger"; +import { + ExperimentEvent, + ExperimentLogFullArgs, + LogFeedbackFullArgs, + mergeGitMetadataSettings, + SpanObjectTypeV3, + SpanTypeAttribute, + isObject, +} from "../util"; +import type { GitMetadataSettings, RepoInfo } from "../sdk-types"; +import iso from "#braintrust-platform"; +import { + getCurrentUnixTimestamp, + isEmpty, + LazyValue, + runCatchFinally, +} from "../util"; +import { + FullLoginOptions, + BraintrustState, + _internalGetGlobalState, + ProjectExperimentMetadata, + validateTags, + Exportable, + enrichAttachments, + ObjectMetadata, + validateAndSanitizeExperimentLogFullArgs, + Span, + StartSpanArgs, + SetCurrentArg, + withCurrent, + logError, + SpanImpl, + startSpanParentArgs, + FailedHTTPResponse, + logFeedbackImpl, + updateSpanImpl, + getSpanComponentsClass, + DefaultMetadataType, + EvalCase, +} from "./core"; +import { + AnyDataset, + DatasetRef, + Dataset, + serializeDatasetForExperiment, +} from "./datasets"; +import { RemoteEvalParameters } from "./prompts"; +import { ObjectFetcher } from "./objects"; + +type InitOpenOption = { + open?: IsOpen; +}; + +export interface ParametersRef { + id: string; + version?: string; +} + +export type InitOptions = FullLoginOptions & { + experiment?: string; + description?: string; + dataset?: AnyDataset | DatasetRef; + _internal_btql?: Record; + parameters?: ParametersRef | RemoteEvalParameters; + update?: boolean; + baseExperiment?: string; + isPublic?: boolean; + metadata?: Record; + tags?: string[]; + gitMetadataSettings?: GitMetadataSettings; + projectId?: string; + baseExperimentId?: string; + repoInfo?: RepoInfo; + setCurrent?: boolean; + state?: BraintrustState; +} & InitOpenOption; + +export type FullInitOptions = { + project?: string; +} & InitOptions; + +function getExperimentDatasetFilter({ + dataset, + _internal_btql, +}: { + dataset?: AnyDataset | DatasetRef; + _internal_btql?: Record; +}): Record | undefined { + if (_internal_btql !== undefined) { + return _internal_btql; + } + + if (!(dataset instanceof Dataset)) { + return undefined; + } + + const datasetFilter = Reflect.get(dataset, "_internal_btql"); + return isObject(datasetFilter) ? datasetFilter : undefined; +} + +type InitializedExperiment = + IsOpen extends true ? ReadonlyExperiment : Experiment; + +/** + * Log in, and then initialize a new experiment in a specified project. If the project does not exist, it will be created. + * + * @param options Options for configuring init(). + * @param options.project The name of the project to create the experiment in. Must specify at least one of `project` or `projectId`. + * @param options.experiment The name of the experiment to create. If not specified, a name will be generated automatically. + * @param options.description An optional description of the experiment. + * @param options.dataset (Optional) A dataset to associate with the experiment. You can pass in the name of the dataset (in the same project) or a dataset object (from any project). + * @param options.update If the experiment already exists, continue logging to it. If it does not exist, creates the experiment with the specified arguments. + * @param options.baseExperiment An optional experiment name to use as a base. If specified, the new experiment will be summarized and compared to this experiment. Otherwise, it will pick an experiment by finding the closest ancestor on the default (e.g. main) branch. + * @param options.isPublic An optional parameter to control whether the experiment is publicly visible to anybody with the link or privately visible to only members of the organization. Defaults to private. + * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. + * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. In Node.js, if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. + * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. + * @param options.metadata (Optional) A dictionary with additional data about the test example, model outputs, or just about anything else that's relevant, that you can use to help find and analyze examples later. For example, you could log the `prompt`, example's `id`, or anything else that would be useful to slice/dice later. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. + * @param options.gitMetadataSettings (Optional) Settings for collecting git metadata. By default, Braintrust collects the git metadata fields allowed by your organization's git metadata settings. If those settings are absent, git metadata is not collected unless this option is set. + * @param setCurrent If true (the default), set the global current-experiment to the newly-created one. + * @param options.open If the experiment already exists, open it in read-only mode. Throws an error if the experiment does not already exist. + * @param options.projectId The id of the project to create the experiment in. This takes precedence over `project` if specified. + * @param options.baseExperimentId An optional experiment id to use as a base. If specified, the new experiment will be summarized and compared to this. This takes precedence over `baseExperiment` if specified. + * @param options.repoInfo (Optional) Explicitly specify the git metadata for this experiment. This takes precedence over `gitMetadataSettings` if specified. + * @param options.tags (Optional) A list of tags to attach to the experiment. + * @returns The newly created Experiment. + */ +export function init( + options: Readonly>, +): InitializedExperiment { + const { + project, + experiment, + description, + dataset, + _internal_btql, + parameters, + baseExperiment, + isPublic, + open, + update, + appUrl, + apiKey, + orgName, + forceLogin, + fetch, + metadata, + tags, + gitMetadataSettings, + projectId, + baseExperimentId, + repoInfo, + state: stateArg, + } = options; + + if (!project && !projectId) { + throw new Error("Must specify at least one of project or projectId"); + } + + if (open && update) { + throw new Error("Cannot open and update an experiment at the same time"); + } + + const state = stateArg ?? _internalGetGlobalState(); + state._internalSetTraceContextSigningSecret(apiKey); + + // Ensure unlimited queue for init() calls (experiments) + // Experiments should never drop data + state.enforceQueueSizeLimit(false); + + if (open) { + if (isEmpty(experiment)) { + throw new Error(`Cannot open an experiment without specifying its name`); + } + + const lazyMetadata: LazyValue = new LazyValue( + async () => { + await state.login({ apiKey, appUrl, orgName, fetch, forceLogin }); + const args: Record = { + project_name: project, + project_id: projectId, + org_name: state.orgName, + experiment_name: experiment, + }; + + const response = await state + .appConn() + .post_json("api/experiment/get", args); + + if (response.length === 0) { + throw new Error( + `Experiment ${experiment} not found in project ${ + projectId ?? project + }.`, + ); + } + + const info = response[0]; + return { + project: { + id: info.project_id, + name: project ?? "UNKNOWN_PROJECT", + fullInfo: {}, + }, + experiment: { + id: info.id, + name: info.name, + fullInfo: info, + }, + }; + }, + ); + + return new ReadonlyExperiment( + stateArg ?? _internalGetGlobalState(), + lazyMetadata, + ) as InitializedExperiment; + } + + const lazyMetadata: LazyValue = new LazyValue( + async () => { + await state.login({ apiKey, appUrl, orgName }); + const args: Record = { + project_name: project, + project_id: projectId, + org_id: state.orgId, + update, + }; + + if (experiment) { + args["experiment_name"] = experiment; + } + + if (description) { + args["description"] = description; + } + + const repoInfoArg = await (async (): Promise => { + if (repoInfo) { + return repoInfo; + } + const mergedGitMetadataSettings = + state.gitMetadataSettings == null + ? (gitMetadataSettings ?? { collect: "none" as const }) + : mergeGitMetadataSettings( + state.gitMetadataSettings, + gitMetadataSettings ?? { collect: "all" as const }, + ); + return await iso.getRepoInfo(mergedGitMetadataSettings); + })(); + + if (repoInfoArg) { + args["repo_info"] = repoInfoArg; + } + + if (baseExperimentId) { + args["base_exp_id"] = baseExperimentId; + } else if (baseExperiment) { + args["base_experiment"] = baseExperiment; + } else { + args["ancestor_commits"] = await iso.getPastNAncestors(); + } + + if (dataset !== undefined) { + const datasetSelection = await serializeDatasetForExperiment({ + dataset, + state, + }); + args["dataset_id"] = datasetSelection.datasetId; + if (datasetSelection.datasetVersion !== undefined) { + args["dataset_version"] = datasetSelection.datasetVersion; + } + } + + const datasetFilter = getExperimentDatasetFilter({ + dataset, + _internal_btql, + }); + if (datasetFilter !== undefined) { + args["internal_metadata"] = { + dataset_filter: datasetFilter, + }; + } + + if (parameters !== undefined) { + if (RemoteEvalParameters.isParameters(parameters)) { + args["parameters_id"] = parameters.id; + args["parameters_version"] = parameters.version; + } else { + args["parameters_id"] = parameters.id; + if (parameters.version !== undefined) { + args["parameters_version"] = parameters.version; + } + } + } + + if (isPublic !== undefined) { + args["public"] = isPublic; + } + + if (metadata) { + args["metadata"] = metadata; + } + + if (tags) { + validateTags(tags); + args["tags"] = tags; + } + + let response = null; + while (true) { + try { + response = await state + .appConn() + .post_json("api/experiment/register", args); + break; + } catch (e: any) { + if ( + args["base_experiment"] && + `${"data" in e && e.data}`.includes("base experiment") + ) { + debugLogger + .forState(state) + .warn(`Base experiment ${args["base_experiment"]} not found.`); + delete args["base_experiment"]; + } else { + throw e; + } + } + } + + return { + project: { + id: response.project.id, + name: response.project.name, + fullInfo: response.project, + }, + experiment: { + id: response.experiment.id, + name: response.experiment.name, + created: response.experiment.created, + fullInfo: response.experiment, + }, + }; + }, + ); + + const ret = new Experiment( + state, + lazyMetadata, + dataset !== undefined ? (dataset as AnyDataset) : undefined, + ); + if (options.setCurrent ?? true) { + state.currentExperiment = ret; + } + return ret as InitializedExperiment; +} + +/** + * An experiment is a collection of logged events, such as model inputs and outputs, which represent + * a snapshot of your application at a particular point in time. An experiment is meant to capture more + * than just the model you use, and includes the data you use to test, pre- and post- processing code, + * comparison metrics (scores), and any other metadata you want to include. + * + * Experiments are associated with a project, and two experiments are meant to be easily comparable via + * their `inputs`. You can change the attributes of the experiments in a project (e.g. scoring functions) + * over time, simply by changing what you log. + * + * You should not create `Experiment` objects directly. Instead, use the `braintrust.init()` method. + */ +export class Experiment + extends ObjectFetcher + implements Exportable +{ + private readonly lazyMetadata: LazyValue; + public readonly dataset?: AnyDataset; + private lastStartTime: number; + private lazyId: LazyValue; + private calledStartSpan: boolean; + private state: BraintrustState; + + // For type identification. + public kind = "experiment" as const; + /** @internal Cross-bundle identification without importing experiments into tracing. */ + public readonly __braintrust_experiment_marker = true; + + constructor( + state: BraintrustState, + lazyMetadata: LazyValue, + dataset?: AnyDataset, + ) { + super("experiment", undefined, (r) => enrichAttachments(r, state)); + this.lazyMetadata = lazyMetadata; + this.dataset = dataset; + this.lastStartTime = getCurrentUnixTimestamp(); + this.lazyId = new LazyValue(() => this.id); + this.calledStartSpan = false; + this.state = state; + } + + public get id(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).experiment.id; + })(); + } + + public get loggingState(): BraintrustState { + return this.state; + } + + /** + * Wait for the experiment ID to be resolved. This is useful for ensuring the ID + * is available synchronously in child spans (for OTEL parent attributes). + * @internal + */ + public async _waitForId(): Promise { + await this.lazyId.get().catch(() => { + // Ignore errors + }); + } + + public get name(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).experiment.name; + })(); + } + + public get project(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).project; + })(); + } + + public async _getBaseExperimentId(): Promise { + const baseExperimentId = (await this.lazyMetadata.get()).experiment + .fullInfo["base_exp_id"]; + return typeof baseExperimentId === "string" && baseExperimentId + ? baseExperimentId + : undefined; + } + + private parentObjectType() { + return SpanObjectTypeV3.EXPERIMENT; + } + + protected async getState(): Promise { + // Ensure the login state is populated by awaiting lazyMetadata. + await this.lazyMetadata.get(); + return this.state; + } + + /** + * Log a single event to the experiment. The event will be batched and uploaded behind the scenes. + * + * @param event The event to log. + * @param event.input: The arguments that uniquely define a test case (an arbitrary, JSON serializable object). Later on, Braintrust will use the `input` to know whether two test cases are the same between experiments, so they should not contain experiment-specific state. A simple rule of thumb is that if you run the same experiment twice, the `input` should be identical. + * @param event.output: The output of your application, including post-processing (an arbitrary, JSON serializable object), that allows you to determine whether the result is correct or not. For example, in an app that generates SQL queries, the `output` should be the _result_ of the SQL query generated by the model, not the query itself, because there may be multiple valid queries that answer a single question. + * @param event.expected: (Optional) The ground truth value (an arbitrary, JSON serializable object) that you'd compare to `output` to determine if your `output` value is correct or not. Braintrust currently does not compare `output` to `expected` for you, since there are so many different ways to do that correctly. Instead, these values are just used to help you navigate your experiments while digging into analyses. However, we may later use these values to re-score outputs or fine-tune your models. + * @param event.error: (Optional) The error that occurred, if any. If you use tracing to run an experiment, errors are automatically logged when your code throws an exception. + * @param event.scores: A dictionary of numeric values (between 0 and 1) to log. The scores should give you a variety of signals that help you determine how accurate the outputs are compared to what you expect and diagnose failures. For example, a summarization app might have one score that tells you how accurate the summary is, and another that measures the word similarity between the generated and grouth truth summary. The word similarity score could help you determine whether the summarization was covering similar concepts or not. You can use these scores to help you sort, filter, and compare experiments. + * @param event.metadata: (Optional) a dictionary with additional data about the test example, model outputs, or just about anything else that's relevant, that you can use to help find and analyze examples later. For example, you could log the `prompt`, example's `id`, or anything else that would be useful to slice/dice later. The values in `metadata` can be any JSON-serializable type, but its keys must be strings. + * @param event.metrics: (Optional) a dictionary of metrics to log. The following keys are populated automatically: "start", "end". + * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you. + * @param event.dataset_record_id: (Optional) the id of the dataset record that this event is associated with. This field is required if and only if the experiment is associated with a dataset. This field is unused and will be removed in a future version. + * @param options Additional logging options + * @param options.allowConcurrentWithSpans in rare cases where you need to log at the top level separately from spans on the experiment elsewhere, set this to true. + * @returns The `id` of the logged event. + */ + public log( + event: Readonly, + options?: { allowConcurrentWithSpans?: boolean }, + ): string { + if (this.calledStartSpan && !options?.allowConcurrentWithSpans) { + throw new Error( + "Cannot run toplevel `log` method while using spans. To log to the span, call `experiment.traced` and then log with `span.log`", + ); + } + + event = validateAndSanitizeExperimentLogFullArgs(event); + const span = this.startSpanImpl({ startTime: this.lastStartTime, event }); + this.lastStartTime = span.end(); + return span.id; + } + + /** + * Create a new toplevel span underneath the experiment. The name defaults to "root". + * + * See {@link Span.traced} for full details. + */ + public traced( + callback: (span: Span) => R, + args?: StartSpanArgs & SetCurrentArg, + ): R { + const { setCurrent, ...argsRest } = args ?? {}; + const span = this.startSpan(argsRest); + + return runCatchFinally( + () => { + if (setCurrent ?? true) { + return withCurrent(span, callback); + } else { + return callback(span); + } + }, + (error) => { + logError(span, error); + throw error; + }, + () => span.end(), + ); + } + + /** + * Lower-level alternative to `traced`. This allows you to start a span yourself, and can be useful in situations + * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span", + * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead. + * + * See {@link traced} for full details. + */ + public startSpan(args?: StartSpanArgs): Span { + this.calledStartSpan = true; + return this.startSpanImpl(args); + } + + private startSpanImpl(args?: StartSpanArgs): Span { + return new SpanImpl({ + ...args, + // Sometimes `args` gets passed directly into this function, and it contains an undefined value for `state`. + // To ensure that we always use this experiment's state, we override the `state` argument no matter what. + state: this.state, + ...startSpanParentArgs({ + state: this.state, + parent: args?.parent, + parentObjectType: this.parentObjectType(), + parentObjectId: this.lazyId, + parentComputeObjectMetadataArgs: undefined, + parentSpanIds: undefined, + propagatedEvent: args?.propagatedEvent, + }), + defaultRootType: SpanTypeAttribute.EVAL, + }); + } + + public async fetchBaseExperiment(): Promise<{ id: any; name: any } | null> { + const state = await this.getState(); + const conn = state.appConn(); + + try { + const resp = await conn.post("/api/base_experiment/get_id", { + id: await this.id, + }); + + const base = await resp.json(); + return { + id: base["base_exp_id"], + name: base["base_exp_name"], + }; + } catch (e) { + if (e instanceof FailedHTTPResponse && e.status === 400) { + // No base experiment + return null; + } else { + throw e; + } + } + } + + /** + * Summarize the experiment, including the scores (compared to the closest reference experiment) and metadata. + * + * @param options Options for summarizing the experiment. + * @param options.summarizeScores Whether to summarize the scores. If False, only the metadata will be returned. + * @param options.comparisonExperimentId The experiment to compare against. If None, the most recent experiment on the origin's main branch will be used. + * @returns A summary of the experiment, including the scores (compared to the closest reference experiment) and metadata. + */ + public async summarize( + options: { + readonly summarizeScores?: boolean; + readonly comparisonExperimentId?: string; + } = {}, + ): Promise { + const { + summarizeScores = true, + comparisonExperimentId: comparisonExperimentIdOpt, + } = options || {}; + let comparisonExperimentId = comparisonExperimentIdOpt; + + const state = await this.getState(); + const projectUrl = `${state.appPublicUrl}/app/${encodeURIComponent( + state.orgName!, + )}/p/${encodeURIComponent((await this.project).name)}`; + const experimentUrl = `${projectUrl}/experiments/${encodeURIComponent( + await this.name, + )}`; + + let scores: Record | undefined = undefined; + let metrics: Record | undefined = undefined; + let comparisonExperimentName = undefined; + if (summarizeScores) { + await this.flush(); + if (comparisonExperimentId === undefined) { + const baseExperiment = await this.fetchBaseExperiment(); + if (baseExperiment !== null) { + comparisonExperimentId = baseExperiment.id; + comparisonExperimentName = baseExperiment.name; + } + } else { + try { + const comparisonExperiment = await state + .apiConn() + .get_json(`v1/experiment/${comparisonExperimentId}`); + if (typeof comparisonExperiment["name"] === "string") { + comparisonExperimentName = comparisonExperiment["name"]; + } + } catch { + // If the explicit comparison name lookup fails, still summarize by ID. + } + } + + try { + const results = await state.apiConn().get_json( + "/experiment-comparison2", + { + experiment_id: await this.id, + base_experiment_id: comparisonExperimentId, + }, + 3, + ); + + scores = results["scores"]; + metrics = results["metrics"]; + } catch (e) { + debugLogger + .forState(state) + .warn( + `Failed to fetch experiment scores and metrics: ${e}\n\nView complete results in Braintrust or run experiment.summarize() again.`, + ); + scores = {}; + metrics = {}; + } + } + + return { + projectName: (await this.project).name, + experimentName: await this.name, + projectId: (await this.project).id, + experimentId: await this.id, + projectUrl: projectUrl, + experimentUrl: experimentUrl, + comparisonExperimentName: comparisonExperimentName, + scores: scores ?? {}, + metrics: metrics ?? {}, + }; + } + + /** + * Log feedback to an event in the experiment. Feedback is used to save feedback scores, set an expected value, or add a comment. + * + * @param event + * @param event.id The id of the event to log feedback for. This is the `id` returned by `log` or accessible as the `id` field of a span. + * @param event.scores (Optional) a dictionary of numeric values (between 0 and 1) to log. These scores will be merged into the existing scores for the event. + * @param event.expected (Optional) the ground truth value (an arbitrary, JSON serializable object) that you'd compare to `output` to determine if your `output` value is correct or not. + * @param event.comment (Optional) an optional comment string to log about the event. + * @param event.metadata (Optional) a dictionary with additional data about the feedback. If you have a `user_id`, you can log it here and access it in the Braintrust UI. Note, this metadata does not correspond to the main event itself, but rather the audit log attached to the event. + * @param event.source (Optional) the source of the feedback. Must be one of "external" (default), "app", or "api". + */ + public logFeedback(event: LogFeedbackFullArgs): void { + logFeedbackImpl(this.state, this.parentObjectType(), this.lazyId, event); + } + + /** + * Update a span in the experiment using its id. It is important that you only update a span once the original span has been fully written and flushed, + * since otherwise updates to the span may conflict with the original span. + * + * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields. + */ + public updateSpan( + event: Omit, "id"> & + Required>, + ): void { + const { id, root_span_id, span_id, ...eventRest } = event; + if (!id) { + throw new Error("Span id is required to update a span"); + } + updateSpanImpl({ + state: this.state, + parentObjectType: this.parentObjectType(), + parentObjectId: this.lazyId, + id, + root_span_id, + span_id, + event: eventRest, + }); + } + + /** + * Return a serialized representation of the experiment that can be used to start subspans in other places. + * + * See {@link Span.startSpan} for more details. + */ + public async export(): Promise { + return new (getSpanComponentsClass())({ + object_type: this.parentObjectType(), + object_id: await this.id, + }).toStr(); + } + + /** + * Return this experiment's Braintrust parent string (`experiment_id:`) for + * the `braintrust.parent` baggage entry, or undefined when it cannot be + * determined synchronously. + */ + public _getOtelParent(): string | undefined { + const id = this.lazyId.getSync().value; + return id ? `experiment_id:${id}` : undefined; + } + + /** + * Flush any pending rows to the server. + */ + async flush(): Promise { + return await this.state.bgLogger().flush(); + } +} + +/** + * A read-only view of an experiment, initialized by passing `open: true` to `init()`. + */ +export class ReadonlyExperiment extends ObjectFetcher { + constructor( + private state: BraintrustState, + private readonly lazyMetadata: LazyValue, + ) { + super("experiment", undefined, (r) => enrichAttachments(r, state)); + } + + public get id(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).experiment.id; + })(); + } + + public get name(): Promise { + return (async () => { + return (await this.lazyMetadata.get()).experiment.name; + })(); + } + + public get loggingState(): BraintrustState { + return this.state; + } + + protected async getState(): Promise { + // Ensure the login state is populated by awaiting lazyMetadata. + await this.lazyMetadata.get(); + return this.state; + } + + public async *asDataset< + Input, + Expected, + Metadata = DefaultMetadataType, + >(options?: { + batchSize?: number; + }): AsyncGenerator> { + const records = this.fetch(options); + + for await (const record of records) { + const isRoot = record.is_root ?? record.root_span_id === record.span_id; + if (!isRoot) { + continue; + } + + const { output, expected: expectedRecord, metadata } = record; + const expected = (expectedRecord ?? output) as Expected; + + // Note: We always include expected and metadata fields to maintain type signature alignment. + // This ensures that when the type signature includes `| null | undefined`, the fields + // are still present in the runtime object. While this may incorrectly include fields + // when Metadata/Expected = void, it's preferable to incorrectly excluding them when + // the type signature expects them to be present. + yield { + input: record.input as Input, + tags: record.tags, + expected: expected as Expected, + metadata: metadata as Metadata, + } as unknown as EvalCase; + } + } +} + +/** + * Summary of a score's performance. + * @property name Name of the score. + * @property score Average score across all examples. + * @property diff Difference in score between the current and reference experiment. + * @property improvements Number of improvements in the score. + * @property regressions Number of regressions in the score. + */ +export interface ScoreSummary { + name: string; + score: number; + diff?: number; + improvements: number; + regressions: number; +} + +/** + * Summary of a metric's performance. + * @property name Name of the metric. + * @property metric Average metric across all examples. + * @property unit Unit label for the metric. + * @property diff Difference in metric between the current and reference experiment. + * @property improvements Number of improvements in the metric. + * @property regressions Number of regressions in the metric. + */ +export interface MetricSummary { + name: string; + metric: number; + unit: string; + diff?: number; + improvements: number; + regressions: number; +} + +/** + * Summary of an experiment's scores and metadata. + * @property projectName Name of the project that the experiment belongs to. + * @property experimentName Name of the experiment. + * @property experimentId ID of the experiment. May be `undefined` if the eval was run locally. + * @property projectUrl URL to the project's page in the Braintrust app. + * @property experimentUrl URL to the experiment's page in the Braintrust app. + * @property comparisonExperimentName The experiment scores are baselined against. + * @property scores Summary of the experiment's scores. + */ +export interface ExperimentSummary { + projectName: string; + experimentName: string; + projectId?: string; + experimentId?: string; + projectUrl?: string; + experimentUrl?: string; + comparisonExperimentName?: string; + scores: Record; + metrics?: Record; +} diff --git a/js/src/logger/lifecycle.test.ts b/js/src/logger/lifecycle.test.ts new file mode 100644 index 000000000..1dd0613a8 --- /dev/null +++ b/js/src/logger/lifecycle.test.ts @@ -0,0 +1,30 @@ +import { expect, test, vi } from "vite-plus/test"; + +test("flush does not initialize an unused SDK", async () => { + const key = Symbol.for("braintrust-state"); + const globals = globalThis as typeof globalThis & { [key: symbol]: unknown }; + const previous = globals[key]; + delete globals[key]; + vi.resetModules(); + try { + const sdk = await import("./core"); + await sdk.flush(); + expect(globals[key]).toBeUndefined(); + } finally { + if (previous !== undefined) globals[key] = previous; + } +}); + +test("flush finds state initialized by another SDK copy", async () => { + const first = await import("./core"); + const logger = first._internalGetGlobalState().bgLogger(); + const flush = vi.spyOn(logger, "flush").mockResolvedValue(undefined); + vi.resetModules(); + try { + const second = await import("./core"); + await second.flush(); + expect(flush).toHaveBeenCalledOnce(); + } finally { + flush.mockRestore(); + } +}); diff --git a/js/src/logger/objects.ts b/js/src/logger/objects.ts new file mode 100644 index 000000000..0490146e9 --- /dev/null +++ b/js/src/logger/objects.ts @@ -0,0 +1,207 @@ +/// +import { TRANSACTION_ID_FIELD, TransactionId } from "../util"; +import { BraintrustState, BTQL_HTTP_RETRIES } from "./core"; + +export type WithTransactionId = R & { + [TRANSACTION_ID_FIELD]: TransactionId; +}; + +export const DEFAULT_FETCH_BATCH_SIZE = 1000; + +export const MAX_BTQL_ITERATIONS = 10000; + +export class ObjectFetcher implements AsyncIterable< + WithTransactionId +> { + private _fetchedData: WithTransactionId[] | undefined = undefined; + + constructor( + private objectType: + | "dataset" + | "experiment" + | "project_logs" + | "playground_logs", + private pinnedVersion: string | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private mutateRecord?: (r: any) => WithTransactionId, + private _internal_btql?: Record, + private _internalBrainstoreRealtime = true, + ) {} + + public get id(): Promise { + throw new Error("ObjectFetcher subclasses must have an 'id' attribute"); + } + + protected async getState(): Promise { + throw new Error("ObjectFetcher subclasses must have a 'getState' method"); + } + + protected getPinnedVersion(): string | undefined { + return this.pinnedVersion; + } + + protected setPinnedVersion(pinnedVersion: string | undefined): void { + this.pinnedVersion = pinnedVersion; + } + + protected getInternalBtql(): Record | undefined { + return this._internal_btql; + } + + private async *fetchRecordsFromApi( + batchSize: number | undefined, + ): AsyncGenerator> { + const state = await this.getState(); + const objectId = await this.id; + const batchLimit = batchSize ?? DEFAULT_FETCH_BATCH_SIZE; + const internalLimit = getInternalBtqlLimit(this._internal_btql); + let remainingLimit = internalLimit; + const internalBtqlWithoutReservedQueryKeys = Object.fromEntries( + Object.entries(this._internal_btql ?? {}).filter( + ([key]) => + key !== "cursor" && + key !== "limit" && + key !== "select" && + key !== "from", + ), + ); + let cursor = undefined; + let iterations = 0; + while (true) { + if (remainingLimit !== undefined && remainingLimit <= 0) { + return; + } + const limit = + remainingLimit === undefined + ? batchLimit + : Math.min(batchLimit, remainingLimit); + const resp = await state.apiConn().post( + `btql`, + { + query: { + select: [ + { + op: "star", + }, + ], + from: { + op: "function", + name: { + op: "ident", + name: [this.objectType], + }, + args: [ + { + op: "literal", + value: objectId, + }, + ], + }, + cursor, + limit, + ...internalBtqlWithoutReservedQueryKeys, + }, + use_columnstore: false, + brainstore_realtime: this._internalBrainstoreRealtime, + query_source: `js_sdk_object_fetcher_${this.objectType}`, + ...(this.pinnedVersion !== undefined + ? { + version: this.pinnedVersion, + } + : {}), + }, + { headers: { "Accept-Encoding": "gzip" } }, + BTQL_HTTP_RETRIES, + ); + const respJson = await resp.json(); + const mutate = this.mutateRecord; + for (const record of respJson.data ?? []) { + if (remainingLimit !== undefined && remainingLimit <= 0) { + return; + } + const mutatedRecord = mutate + ? mutate(record) + : (record as WithTransactionId); + if (remainingLimit !== undefined) { + remainingLimit--; + } + yield mutatedRecord; + } + if (!respJson.cursor) { + break; + } + cursor = respJson.cursor; + iterations++; + if (iterations > MAX_BTQL_ITERATIONS) { + throw new Error("Too many BTQL iterations"); + } + } + } + + /** + * Fetch all records from the object. + * + * @param options Optional parameters for fetching. + * @param options.batchSize The number of records to fetch per request. Defaults to 1000. + * @returns An async generator of records. + */ + async *fetch(options?: { + batchSize?: number; + }): AsyncGenerator> { + if (this._fetchedData !== undefined) { + for (const record of this._fetchedData) { + yield record; + } + return; + } + + for await (const record of this.fetchRecordsFromApi(options?.batchSize)) { + yield record; + } + } + + [Symbol.asyncIterator](): AsyncIterator> { + return this.fetch(); + } + + async fetchedData(options?: { + batchSize?: number; + }): Promise[]> { + if (this._fetchedData === undefined) { + const data: WithTransactionId[] = []; + for await (const record of this.fetchRecordsFromApi(options?.batchSize)) { + data.push(record); + } + this._fetchedData = data; + } + return this._fetchedData || []; + } + + clearCache(): void { + this._fetchedData = undefined; + } + + public async version(options?: { + batchSize?: number; + }): Promise { + if (this.pinnedVersion !== undefined) { + return this.pinnedVersion; + } else { + let maxVersion: string | undefined = undefined; + for await (const record of this.fetch(options)) { + const xactId = String(record[TRANSACTION_ID_FIELD] ?? "0"); + if (maxVersion === undefined || xactId > maxVersion) { + maxVersion = xactId; + } + } + return maxVersion; + } + } +} + +function getInternalBtqlLimit( + internalBtql?: Record, +): number | undefined { + const limit = internalBtql?.["limit"]; + return typeof limit === "number" ? limit : undefined; +} diff --git a/js/src/logger/prompts.ts b/js/src/logger/prompts.ts new file mode 100644 index 000000000..6adddbb20 --- /dev/null +++ b/js/src/logger/prompts.ts @@ -0,0 +1,1528 @@ +import { createCacheLayers } from "../prompt-cache/cache-config"; +import { + PromptCache, + type PromptDiskCacheEntry, + type PromptMemoryCacheEntry, +} from "../prompt-cache/prompt-cache"; +import { + ParametersCache, + type ParametersDiskCacheEntry, + type ParametersMemoryCacheEntry, +} from "../prompt-cache/parameters-cache"; +import iso from "#braintrust-platform"; +import type { ResolvedLoaderLoginOptions } from "./core"; +/// +import { debugLogger } from "../debug-logger"; +import { + TRANSACTION_ID_FIELD, + TransactionId, + isArray, + isObject, + getObjValueByPath, +} from "../util"; +import { + BraintrustAttachmentReference as BraintrustAttachmentReferenceSchema, + ChatCompletionTool as chatCompletionToolSchema, + ExternalAttachmentReference as ExternalAttachmentReferenceSchema, + PromptData as promptDataSchema, + Prompt as promptSchema, +} from "../sdk-schemas"; +import type { + ChatCompletionTool, + ModelParams, + ChatCompletionMessageParam as Message, + ChatCompletionOpenAIMessageParam as OpenAIMessage, + PromptData, + FunctionTypeEnum, + PromptSessionEvent, + PromptBlockData, +} from "../sdk-types"; +import { + parseTemplateFormat, + renderTemplateContent, +} from "../template/renderer"; +import type { TemplateFormat } from "../template/registry"; +import { z } from "zod/v3"; +import { isEmpty } from "../util"; +import { prettifyXact } from "../../util/index"; +import type { EvalParameters, InferParameters } from "../eval-parameters"; +import { + FullLoginOptions, + BraintrustState, + LoaderRequestState, + _internalGetGlobalState, + isLoaderCacheFallbackError, + InlineAttachmentReferenceSchema, + resolveAttachmentsToBase64, + ReadonlyAttachment, + BTQL_HTTP_RETRIES, +} from "./core"; + +const BRAINTRUST_PARAMS = [ + "use_cache", + "reasoning_enabled", + "reasoning_budget", +]; + +const parametersRowSchema = z.object({ + id: z.string().uuid(), + _xact_id: z.string(), + project_id: z.string().uuid(), + name: z.string(), + slug: z.string(), + description: z.union([z.string(), z.null()]).optional(), + function_type: z.literal("parameters"), + function_data: z.object({ + type: z.literal("parameters"), + data: z.record(z.unknown()).optional(), + __schema: z.record(z.unknown()), + }), + metadata: z + .union([z.object({}).partial().passthrough(), z.null()]) + .optional(), +}); + +export type ParametersRow = { + id: string; + _xact_id: string; + project_id: string; + name: string; + slug: string; + description?: string | null; + function_type: "parameters"; + function_data: { + type: "parameters"; + data?: Record; + __schema: Record; + }; + metadata?: Record | null; +}; + +export type LoadPromptOptions = FullLoginOptions & { + projectName?: string; + projectId?: string; + slug?: string; + version?: string; + id?: string; + defaults?: DefaultPromptArgs; + noTrace?: boolean; + environment?: string; + state?: BraintrustState; +}; + +type LoadParametersBaseOptions = FullLoginOptions & { + state?: BraintrustState; +}; + +export type LoadParametersByIdOptions = LoadParametersBaseOptions & { + id: string; + version?: string; +}; + +export type LoadParametersByIdWithEnvOptions = LoadParametersBaseOptions & { + id: string; + environment: string; + version?: string; +}; + +export type LoadParametersByProjectNameOptions = LoadParametersBaseOptions & { + projectName: string; + slug: string; + version?: string; +}; + +export type LoadParametersByProjectNameWithEnvOptions = + LoadParametersBaseOptions & { + projectName: string; + slug: string; + environment: string; + version?: string; + }; + +export type LoadParametersByProjectIdOptions = LoadParametersBaseOptions & { + projectId: string; + slug: string; + version?: string; +}; + +export type LoadParametersByProjectIdWithEnvOptions = + LoadParametersBaseOptions & { + projectId: string; + slug: string; + environment: string; + version?: string; + }; + +export type LoadParametersOptions = + | LoadParametersByIdOptions + | LoadParametersByIdWithEnvOptions + | LoadParametersByProjectNameOptions + | LoadParametersByProjectNameWithEnvOptions + | LoadParametersByProjectIdOptions + | LoadParametersByProjectIdWithEnvOptions; + +type LoadParametersImplementationOptions = LoadParametersBaseOptions & { + projectName?: string; + projectId?: string; + slug?: string; + version?: string; + id?: string; + environment?: string; +}; + +type LoaderRequestResult = + | ({ ok: true; response: T } & LoaderCacheViews) + | ({ ok: false; error: unknown } & LoaderCacheViews); + +async function runCredentialScopedLoaderRequest( + state: BraintrustState, + loginOptions: Pick< + FullLoginOptions, + "apiKey" | "appUrl" | "orgName" | "fetch" | "forceLogin" + >, + request: (requestState: LoaderRequestState) => Promise, +): Promise> { + const resolvedLoginOptions = + await state._internalResolveLoaderLoginOptions(loginOptions); + let cacheViews = getLoaderCacheViews(state, resolvedLoginOptions); + + try { + const requestState = + await state._internalGetLoaderState(resolvedLoginOptions); + cacheViews = getLoaderCacheViews(state, resolvedLoginOptions, requestState); + return { + ok: true, + response: await request(requestState), + ...cacheViews, + }; + } catch (error) { + return { ok: false, error, ...cacheViews }; + } +} + +/** + * Load a prompt from the specified project. + * + * @param options Options for configuring loadPrompt(). + * @param options.projectName The name of the project to load the prompt from. Must specify at least one of `projectName` or `projectId`. + * @param options.projectId The id of the project to load the prompt from. This takes precedence over `projectName` if specified. + * @param options.slug The slug of the prompt to load. + * @param options.version An optional version of the prompt (to read). If not specified, the latest version will be used. + * @param options.environment Fetch the version of the prompt assigned to the specified environment (e.g. "production", "staging"). If both `version` and `environment` are provided, `version` takes precedence. + * @param options.id The id of a specific prompt to load. If specified, this takes precedence over all other parameters (project and slug). + * @param options.defaults (Optional) A dictionary of default values to use when rendering the prompt. Prompt values will override these defaults. + * @param options.noTrace If true, do not include logging metadata for this prompt when build() is called. + * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. + * @param options.apiKey The API key to use for this request, independently of any existing global login. If the parameter is not specified, will use an existing login or try the `BRAINTRUST_API_KEY` environment variable. In Node.js, + * if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. If no API key is specified, will prompt the user to login. + * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. + * @returns The prompt object. + * @throws If the prompt is not found. + * @throws If multiple prompts are found with the same slug in the same project (this should never happen). + * + * @example + * ```javascript + * const prompt = await loadPrompt({ + * projectName: "My Project", + * slug: "my-prompt", + * }); + * ``` + */ +export async function loadPrompt({ + projectName, + projectId, + slug, + version, + environment, + id, + defaults, + noTrace = false, + appUrl, + apiKey, + orgName, + fetch, + forceLogin, + state: stateArg, +}: LoadPromptOptions): Promise> { + const versionOrEnvironment = version + ? { version } + : environment + ? { environment } + : {}; + if (id) { + // When loading by ID, we don't need project or slug + } else if (isEmpty(projectName) && isEmpty(projectId)) { + throw new Error("Must specify either projectName or projectId"); + } else if (isEmpty(slug)) { + throw new Error("Must specify slug"); + } + + const state = stateArg ?? _internalGetGlobalState(); + const result = await runCredentialScopedLoaderRequest( + state, + { orgName, apiKey, appUrl, fetch, forceLogin }, + async (requestState) => { + let response; + if (id) { + // Load prompt by ID using the /v1/prompt/{id} endpoint + response = await requestState + .apiConn() + .get_json(`v1/prompt/${id}`, versionOrEnvironment); + // Wrap single prompt response in objects array to match list API format + if (response) { + response = { objects: [response] }; + } + } else { + response = await requestState.apiConn().get_json("v1/prompt", { + project_name: projectName, + project_id: projectId, + slug, + ...versionOrEnvironment, + }); + } + return response; + }, + ); + const { promptCache } = result; + if (!result.ok) { + const e = result.error; + if (!isLoaderCacheFallbackError(e)) { + throw e; + } + // If environment or version was specified, don't fall back to cache + if (version || environment) { + throw new Error(`Prompt not found with specified parameters: ${e}`); + } + + debugLogger + .forState(state) + .warn("Failed to load prompt, attempting to fall back to cache:", e); + let prompt; + if (id) { + prompt = await promptCache.get({ id }); + if (!prompt) { + throw new Error( + `Prompt with id ${id} not found (not found on server or in local cache): ${e}`, + ); + } + } else { + prompt = await promptCache.get({ + slug, + projectId, + projectName, + version: version ?? "latest", + }); + if (!prompt) { + throw new Error( + `Prompt ${slug} (version ${version ?? "latest"}) not found in ${[ + projectName ?? projectId, + ]} (not found on server or in local cache): ${e}`, + ); + } + } + return prompt; + } + const { response } = result; + + if (!("objects" in response) || response.objects.length === 0) { + if (id) { + throw new Error(`Prompt with id ${id} not found.`); + } else { + throw new Error( + `Prompt ${slug} not found in ${[projectName ?? projectId]}`, + ); + } + } else if (response.objects.length > 1) { + if (id) { + throw new Error( + `Multiple prompts found with id ${id}. This should never happen.`, + ); + } else { + throw new Error( + `Multiple prompts found with slug ${slug} in project ${ + projectName ?? projectId + }. This should never happen.`, + ); + } + } + + const metadata = promptSchema.parse(response["objects"][0]); + const prompt = new Prompt(metadata, defaults || {}, noTrace); + try { + if (id) { + await promptCache.set({ id }, prompt); + } else if (slug) { + await promptCache.set( + { slug, projectId, projectName, version: version ?? "latest" }, + prompt, + ); + } + } catch (e) { + debugLogger.forState(state).warn("Failed to set prompt in cache:", e); + } + return prompt; +} + +/** + * Load parameters from the specified project. + * + * @param options Options for configuring loadParameters(). + * @param options.projectName The name of the project to load the parameters from. Must specify at least one of `projectName` or `projectId`. + * @param options.projectId The id of the project to load the parameters from. This takes precedence over `projectName` if specified. + * @param options.slug The slug of the parameters to load. + * @param options.version An optional version of the parameters (to read). If not specified, the latest version will be used. + * @param options.environment Fetch the version of the parameters assigned to the specified environment (e.g. "production", "staging"). If both `version` and `environment` are provided, `version` takes precedence. + * @param options.id The id of specific parameters to load. If specified, this takes precedence over all other parameters (project and slug). + * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. + * @param options.apiKey The API key to use for this request, independently of any existing global login. If the parameter is not specified, will use an existing login or try the `BRAINTRUST_API_KEY` environment variable. In Node.js, if that is unset, will try the nearest `.env.braintrust` file in the current working directory or parent directories. + * @param options.orgName (Optional) The name of a specific organization to connect to. This is useful if you belong to multiple. + * @returns The parameters object. + * @throws If the parameters are not found. + * @throws If multiple parameters are found with the same slug in the same project (this should never happen). + * + * @example + * ```typescript + * const params = await loadParameters({ + * projectName: "My Project", + * slug: "my-parameters", + * }); + * ``` + */ +export function loadParameters( + options: LoadParametersByProjectNameOptions, +): Promise>>; + +export function loadParameters( + options: LoadParametersByProjectNameWithEnvOptions, +): Promise>>; + +export function loadParameters( + options: LoadParametersByProjectIdOptions, +): Promise>>; + +export function loadParameters( + options: LoadParametersByProjectIdWithEnvOptions, +): Promise>>; + +export function loadParameters( + options: LoadParametersByIdOptions, +): Promise>>; + +export function loadParameters( + options: LoadParametersByIdWithEnvOptions, +): Promise>>; + +export async function loadParameters< + S extends EvalParameters = EvalParameters, +>({ + projectName, + projectId, + slug, + version, + environment, + id, + appUrl, + apiKey, + orgName, + fetch, + forceLogin, + state: stateArg, +}: LoadParametersImplementationOptions): Promise< + RemoteEvalParameters> +> { + const versionOrEnvironment = version + ? { version } + : environment + ? { environment } + : {}; + if (id) { + // When loading by ID, we don't need project or slug + } else if (isEmpty(projectName) && isEmpty(projectId)) { + throw new Error("Must specify either projectName or projectId"); + } else if (isEmpty(slug)) { + throw new Error("Must specify slug"); + } + + const state = stateArg ?? _internalGetGlobalState(); + const result = await runCredentialScopedLoaderRequest( + state, + { orgName, apiKey, appUrl, fetch, forceLogin }, + async (requestState) => { + let response; + if (id) { + response = await requestState.apiConn().get_json(`v1/function/${id}`, { + ...versionOrEnvironment, + }); + if (response) { + response = { objects: [response] }; + } + } else { + response = await requestState.apiConn().get_json("v1/function", { + project_name: projectName, + project_id: projectId, + slug, + function_type: "parameters", + ...versionOrEnvironment, + }); + } + return response; + }, + ); + const { parametersCache } = result; + if (!result.ok) { + const e = result.error; + if (!isLoaderCacheFallbackError(e)) { + throw e; + } + if (version || environment) { + throw new Error(`Parameters not found with specified parameters: ${e}`); + } + + debugLogger + .forState(state) + .warn("Failed to load parameters, attempting to fall back to cache:", e); + let parameters; + if (id) { + parameters = await parametersCache.get({ id }); + if (!parameters) { + throw new Error( + `Parameters with id ${id} not found (not found on server or in local cache): ${e}`, + ); + } + } else { + parameters = await parametersCache.get({ + slug, + projectId, + projectName, + version: version ?? "latest", + }); + if (!parameters) { + throw new Error( + `Parameters ${slug} (version ${version ?? "latest"}) not found in ${[ + projectName ?? projectId, + ]} (not found on server or in local cache): ${e}`, + ); + } + } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return parameters as RemoteEvalParameters>; + } + const { response } = result; + + if (!("objects" in response) || response.objects.length === 0) { + if (id) { + throw new Error(`Parameters with id ${id} not found.`); + } else { + throw new Error( + `Parameters ${slug} not found in ${[projectName ?? projectId]}`, + ); + } + } else if (response.objects.length > 1) { + if (id) { + throw new Error( + `Multiple parameters found with id ${id}. This should never happen.`, + ); + } else { + throw new Error( + `Multiple parameters found with slug ${slug} in project ${ + projectName ?? projectId + }. This should never happen.`, + ); + } + } + + const metadata = parametersRowSchema.parse(response["objects"][0]); + const parameters = new RemoteEvalParameters(metadata); + try { + if (id) { + await parametersCache.set({ id }, parameters); + } else if (slug) { + await parametersCache.set( + { slug, projectId, projectName, version: version ?? "latest" }, + parameters, + ); + } + } catch (e) { + debugLogger.forState(state).warn("Failed to set parameters in cache:", e); + } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return parameters as RemoteEvalParameters>; +} + +export type CompiledPromptResponseFormat = + | { type: "json_object" } + | { type: "text" } + | { + type: "json_schema"; + json_schema: { + name: string; + description?: string; + schema?: Record; + strict?: boolean | null; + }; + }; + +// Keep the compiled API independent of the backend's provider parameter union. +// "none" is excluded from reasoning_effort for compatibility with older OpenAI clients. +export interface CompiledPromptParams { + chat_template_kwargs?: { enable_thinking?: boolean; [key: string]: unknown }; + model: string; + max_tokens: number; + temperature?: number; + top_p?: number; + max_completion_tokens?: number; + frequency_penalty?: number; + presence_penalty?: number; + response_format?: CompiledPromptResponseFormat; + tool_choice?: + | "auto" + | "none" + | "required" + | { + type: "function"; + function: { name: string }; + }; + function_call?: "auto" | "none" | { name: string }; + n?: number; + stop?: string[]; + reasoning_effort?: "minimal" | "low" | "medium" | "high"; + verbosity?: "low" | "medium" | "high"; + top_k?: number; + stop_sequences?: string[]; + reasoning_enabled?: boolean; + reasoning_budget?: number; + /** Legacy parameter; prefer max_tokens. */ + max_tokens_to_sample?: number; + maxOutputTokens?: number; + topP?: number; + topK?: number; +} + +export type ChatPrompt = { + messages: OpenAIMessage[]; + tools?: ChatCompletionTool[]; +}; + +export type CompletionPrompt = { + prompt: string; +}; + +export type CompiledPrompt = + CompiledPromptParams & { + span_info?: { + name?: string; + spanAttributes?: Record; + metadata: { + prompt: { + variables: Record; + id: string; + project_id: string; + version: string; + }; + }; + }; + } & (Flavor extends "chat" + ? ChatPrompt + : Flavor extends "completion" + ? CompletionPrompt + : // eslint-disable-next-line @typescript-eslint/no-empty-object-type + {}); + +export interface DefaultPromptArgs + extends + Partial, + Partial, + Partial { + use_cache?: boolean; +} + +function isAttachmentObject(value: unknown): boolean { + return ( + BraintrustAttachmentReferenceSchema.safeParse(value).success || + InlineAttachmentReferenceSchema.safeParse(value).success || + ExternalAttachmentReferenceSchema.safeParse(value).success + ); +} + +// Simple URL validation all braintrust attachment urls or generic links +function isURL(url: string): boolean { + try { + const parsedUrl = new URL(url.trim()); + return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"; + } catch { + return false; + } +} + +/** + * Detect and expand attachment array variables before template rendering. + * Supports both simple variables ({{images}}) and nested paths ({{data.images}}). + * @param content The message content (should be a template variable like "{{images}}" or "{{data.images}}") + * @param variables The variables object with array values + * @returns Array of image_url parts if expansion occurred, null otherwise + */ +function expandAttachmentArrayPreTemplate( + content: unknown, + variables: Record, +): unknown[] | null { + if (typeof content !== "string") return null; + + // Detect {{varName}} or {{nested.path}} pattern (exact match only - no mixed content) + // Supports: {{images}}, {{data.images}}, {{user.profile.images}}, etc. + const match = content.match(/^\{\{\s*([\w.]+)\s*\}\}$/); + if (!match) return null; + + const varPath = match[1]; + + const value = varPath.includes(".") + ? getObjValueByPath(variables, varPath.split(".")) + : variables[varPath]; + + if (!Array.isArray(value)) return null; + + // Check if array contains attachments or image URLs + const allValid = value.every( + (v) => isAttachmentObject(v) || (typeof v === "string" && isURL(v)), + ); + + if (!allValid) return null; + + // Expand directly to image_url parts + return value.map((item) => ({ + type: "image_url" as const, + image_url: { url: item }, + })); +} + +export function renderMessage( + render: (template: string) => string, + message: T, +): T; + +export function renderMessage( + render: (template: string) => string, + message: T, +): T { + return renderMessageImpl(render, message, {}); +} + +export function renderMessageImpl( + render: (template: string) => string, + message: T, + variables?: Record, +): T { + return { + ...message, + ...("content" in message + ? { + content: isEmpty(message.content) + ? undefined + : typeof message.content === "string" + ? render(message.content) + : message.content.flatMap((c) => { + switch (c.type) { + case "text": + return [{ ...c, text: render(c.text) }]; + case "image_url": + if (isObject(c.image_url.url)) { + throw new Error( + "Attachments must be replaced with URLs before calling `build()`", + ); + } + + if (variables) { + const expanded = expandAttachmentArrayPreTemplate( + c.image_url.url, + variables, + ); + if (expanded) { + return expanded; + } + } + + // Otherwise render the URL template normally + return [ + { + ...c, + image_url: { + ...c.image_url, + url: render(c.image_url.url), + }, + }, + ]; + case "input_audio": + return [ + { + ...c, + input_audio: { + ...c.input_audio, + data: render(c.input_audio.data), + }, + }, + ]; + case "file": + return [ + { + ...c, + file: { + ...(c.file.file_data && { + file_data: render(c.file.file_data), + }), + ...(c.file.file_id && { + file_id: render(c.file.file_id), + }), + ...(c.file.filename && { + filename: render(c.file.filename), + }), + }, + }, + ]; + default: + const _exhaustiveCheck: never = c; + return _exhaustiveCheck; + } + }), + } + : {}), + ...("tool_calls" in message + ? { + tool_calls: isEmpty(message.tool_calls) + ? undefined + : message.tool_calls.map((t) => { + return { + type: t.type, + id: render(t.id), + function: { + name: render(t.function.name), + arguments: render(t.function.arguments), + }, + }; + }), + } + : {}), + ...("tool_call_id" in message + ? { + tool_call_id: render(message.tool_call_id), + } + : {}), + }; +} + +export interface PromptMetadata { + name: string; + slug: string; + project_id?: string; + description?: string | null; + created?: string | null; + prompt_data?: PromptData | null; + tags?: string[] | null; + // Preserve the generated prompt row's permissive metadata contract. + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + metadata?: {} | null; + function_type?: FunctionTypeEnum | null; +} + +export type PromptRowWithId< + HasId extends boolean = true, + HasVersion extends boolean = true, +> = PromptMetadata & + (HasId extends true ? { id: string } : { id?: string }) & + (HasVersion extends true ? { _xact_id: string } : { _xact_id?: string }); + +export function deserializePlainStringAsJSON( + s: string, +): { value: any; error: undefined } | { value: string; error: unknown } { + if (s.trim() === "") { + return { value: null, error: undefined }; + } + + try { + return { value: JSON.parse(s), error: undefined }; + } catch (e) { + return { value: s, error: e }; + } +} + +function renderTemplatedObject( + obj: unknown, + args: Record, + options: { strict?: boolean; templateFormat: TemplateFormat }, +): unknown { + if (typeof obj === "string") { + return renderTemplateContent( + obj, + args, + (value) => (typeof value === "string" ? value : JSON.stringify(value)), + { + strict: options.strict, + templateFormat: options.templateFormat, + }, + ); + } else if (isArray(obj)) { + return obj.map((item) => renderTemplatedObject(item, args, options)); + } else if (isObject(obj)) { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [ + key, + renderTemplatedObject(value, args, options), + ]), + ); + } + return obj; +} + +export function renderPromptParams( + params: ModelParams | undefined, + args: Record, + options: { strict?: boolean; templateFormat?: TemplateFormat } = {}, +): ModelParams | undefined { + const templateFormat = parseTemplateFormat(options.templateFormat); + const strict = !!options.strict; + + const schemaParsed = z + .object({ + response_format: z.object({ + type: z.literal("json_schema"), + json_schema: z.object({ + name: z.string(), + description: z.string().optional(), + strict: z.boolean().nullable().optional(), + schema: z.unknown(), + }), + }), + }) + .safeParse(params); + if (schemaParsed.success) { + const rawSchema = schemaParsed.data.response_format.json_schema.schema; + const templatedSchema = renderTemplatedObject(rawSchema, args, { + strict, + templateFormat, + }); + const parsedSchema = + typeof templatedSchema === "string" + ? deserializePlainStringAsJSON(templatedSchema).value + : templatedSchema; + + return { + ...params, + response_format: { + ...schemaParsed.data.response_format, + json_schema: { + ...schemaParsed.data.response_format.json_schema, + schema: parsedSchema, + }, + }, + }; + } + return params; +} + +export interface SerializedPrompt { + metadata: PromptRowWithId | PromptSessionEvent; + defaults: DefaultPromptArgs; + noTrace: boolean; +} + +// Initialized by the class so cache serialization can access private fields +// without exposing a serialization method on the public Prompt API. +export let serializePromptForCache: (prompt: Prompt) => SerializedPrompt; + +export class Prompt< + HasId extends boolean = true, + HasVersion extends boolean = true, +> { + private parsedPromptData: PromptData | undefined; + private hasParsedPromptData = false; + private readonly __braintrust_prompt_marker = true; + + constructor( + private metadata: PromptRowWithId | PromptSessionEvent, + private defaults: DefaultPromptArgs, + private noTrace: boolean, + ) { + void this.__braintrust_prompt_marker; + } + + public get id(): HasId extends true ? string : string | undefined { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return this.metadata.id as HasId extends true ? string : string | undefined; + } + + public get projectId(): string | undefined { + return this.metadata.project_id; + } + + public get name(): string { + return "name" in this.metadata + ? this.metadata.name + : `Playground function ${this.metadata.id}`; + } + + public get slug(): string { + return "slug" in this.metadata ? this.metadata.slug : this.metadata.id; + } + + public get prompt(): PromptData["prompt"] { + return this.getParsedPromptData()?.prompt; + } + + public get version(): HasId extends true + ? TransactionId + : TransactionId | undefined { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return this.metadata[TRANSACTION_ID_FIELD] as HasId extends true + ? TransactionId + : TransactionId | undefined; + } + + public get options(): NonNullable { + return this.getParsedPromptData()?.options || {}; + } + + public get templateFormat(): string | null | undefined { + return this.getParsedPromptData()?.template_format; + } + + public get promptData(): PromptData { + return this.getParsedPromptData()!; + } + + /** + * Build the prompt with the given formatting options. The args you pass in will + * be forwarded to the mustache template that defines the prompt and rendered with + * the `mustache-js` library. + * + * @param buildArgs Args to forward along to the prompt template. + */ + public build( + buildArgs: unknown, + options: { + flavor?: Flavor; + messages?: Message[]; + strict?: boolean; + templateFormat?: TemplateFormat; + } = {}, + ): CompiledPrompt { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return this.runBuild(buildArgs, { + flavor: options.flavor ?? "chat", + messages: options.messages, + strict: options.strict, + templateFormat: options.templateFormat, + }) as CompiledPrompt; + } + + /** + * This is a special build method that first resolves attachment references, and then + * calls the regular build method. You should use this if you are building prompts from + * dataset rows that contain attachments. + * + * @param buildArgs Args to forward along to the prompt template. + */ + public async buildWithAttachments< + Flavor extends "chat" | "completion" = "chat", + >( + buildArgs: unknown, + options: { + flavor?: Flavor; + messages?: Message[]; + strict?: boolean; + state?: BraintrustState; + templateFormat?: TemplateFormat; + } = {}, + ): Promise> { + const hydrated = + buildArgs instanceof Object + ? await resolveAttachmentsToBase64(buildArgs, options.state) + : buildArgs; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return this.runBuild(hydrated, { + flavor: options.flavor ?? "chat", + messages: options.messages, + strict: options.strict, + templateFormat: options.templateFormat, + }) as CompiledPrompt; + } + + private runBuild( + buildArgs: unknown, + options: { + flavor: Flavor; + messages?: Message[]; + strict?: boolean; + templateFormat?: TemplateFormat; + }, + ): CompiledPrompt { + const { flavor } = options; + + const params = Object.fromEntries( + Object.entries({ + ...this.defaults, + ...Object.fromEntries( + Object.entries(this.options.params || {}).filter( + ([k, _v]) => !BRAINTRUST_PARAMS.includes(k), + ), + ), + ...(!isEmpty(this.options.model) + ? { + model: this.options.model, + } + : {}), + }).filter(([key, value]) => key !== "response_format" || value !== null), + ); + + if (!("model" in params) || isEmpty(params.model)) { + throw new Error( + "No model specified. Either specify it in the prompt or as a default", + ); + } + + const spanInfo = this.noTrace + ? {} + : { + span_info: { + metadata: { + prompt: this.id + ? { + variables: buildArgs, + id: this.id, + project_id: this.projectId, + version: this.version, + ...("prompt_session_id" in this.metadata + ? { prompt_session_id: this.metadata.prompt_session_id } + : {}), + } + : undefined, + }, + }, + }; + + const prompt = this.prompt; + + if (!prompt) { + throw new Error("Empty prompt"); + } + + const dictArgParsed = z.record(z.unknown()).safeParse(buildArgs); + const variables: Record = { + input: buildArgs, + ...(dictArgParsed.success ? dictArgParsed.data : {}), + }; + + // Use template_format from prompt data if available, otherwise fall back to the option or default to mustache + const promptDataTemplateFormat = this.templateFormat; + const resolvedTemplateFormat = parseTemplateFormat( + options.templateFormat ?? promptDataTemplateFormat, + ); + + const renderedPrompt = Prompt.renderPrompt({ + prompt, + buildArgs, + options: { ...options, templateFormat: resolvedTemplateFormat }, + }); + + if (flavor === "chat") { + if (renderedPrompt.type !== "chat") { + throw new Error( + "Prompt is a completion prompt. Use buildCompletion() instead", + ); + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + ...renderPromptParams(params, variables, { + strict: options.strict, + templateFormat: resolvedTemplateFormat, + }), + ...spanInfo, + messages: renderedPrompt.messages, + ...(renderedPrompt.tools + ? { + tools: chatCompletionToolSchema + .array() + .parse(JSON.parse(renderedPrompt.tools)), + } + : undefined), + } as CompiledPrompt; + } else if (flavor === "completion") { + if (renderedPrompt.type !== "completion") { + throw new Error(`Prompt is a chat prompt. Use flavor: 'chat' instead`); + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + ...renderPromptParams(params, variables, { + strict: options.strict, + templateFormat: resolvedTemplateFormat, + }), + ...spanInfo, + prompt: renderedPrompt.content, + } as CompiledPrompt; + } else { + throw new Error("never!"); + } + } + + static renderPrompt({ + prompt, + buildArgs, + options, + }: { + prompt: PromptBlockData; + buildArgs: unknown; + options: { + strict?: boolean; + messages?: Message[]; + templateFormat?: TemplateFormat; + }; + }): PromptBlockData { + const escape = (v: unknown) => { + if (v === undefined) { + throw new Error("Missing!"); + } else if (typeof v === "string") { + return v; + } else if (v instanceof ReadonlyAttachment) { + throw new Error( + "Use buildWithAttachments() to build prompts with attachments", + ); + } else { + return JSON.stringify(v); + } + }; + + const dictArgParsed = z.record(z.unknown()).safeParse(buildArgs); + const variables: Record = { + input: buildArgs, + ...(dictArgParsed.success ? dictArgParsed.data : {}), + }; + + const templateFormat = parseTemplateFormat(options.templateFormat); + + if (prompt.type === "chat") { + const render = (template: string) => + renderTemplateContent(template, variables, escape, { + strict: options.strict, + templateFormat: templateFormat, + }); + + const baseMessages = (prompt.messages || []).map((m) => + renderMessageImpl(render, m, variables), + ); + const hasSystemPrompt = baseMessages.some((m) => m.role === "system"); + + const messages: Message[] = [ + ...baseMessages, + ...(options.messages ?? []).filter( + (m) => !(hasSystemPrompt && m.role === "system"), + ), + ]; + + return { + type: "chat", + messages: messages, + ...(prompt.tools?.trim() + ? { + tools: render(prompt.tools), + } + : undefined), + }; + } else if (prompt.type === "completion") { + if (options.messages) { + throw new Error( + "extra messages are not supported for completion prompts", + ); + } + + const content = renderTemplateContent(prompt.content, variables, escape, { + strict: options.strict, + templateFormat: templateFormat, + }); + return { + type: "completion", + content, + }; + } else { + const _: never = prompt; + throw new Error(`Invalid prompt type: ${_}`); + } + } + + private getParsedPromptData(): PromptData | undefined { + if (!this.hasParsedPromptData) { + this.parsedPromptData = promptDataSchema.parse(this.metadata.prompt_data); + this.hasParsedPromptData = true; + } + return this.parsedPromptData!; + } + + public static isPrompt(data: unknown): data is Prompt { + return ( + typeof data === "object" && + data !== null && + "__braintrust_prompt_marker" in data + ); + } + + static { + serializePromptForCache = (prompt) => ({ + metadata: prompt.metadata, + defaults: prompt.defaults, + noTrace: prompt.noTrace, + }); + } + + public static fromPromptData( + name: string, + promptData: PromptData, + ): Prompt { + return new Prompt( + { + name: name, + slug: name, + prompt_data: promptData, + }, + {}, + false, + ); + } +} + +export interface SerializedParameters { + metadata: ParametersRow; +} + +export let serializeParametersForCache: ( + parameters: RemoteEvalParameters, +) => SerializedParameters; + +export class RemoteEvalParameters< + HasId extends boolean = true, + HasVersion extends boolean = true, + T extends Record = Record, +> { + private readonly __braintrust_parameters_marker = true; + + constructor(private metadata: ParametersRow) { + void this.__braintrust_parameters_marker; + } + + public get id(): HasId extends true ? string : string | undefined { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return this.metadata.id as HasId extends true ? string : string | undefined; + } + + public get projectId(): string | undefined { + return this.metadata.project_id; + } + + public get name(): string { + return this.metadata.name; + } + + public get slug(): string { + return this.metadata.slug; + } + + public get version(): HasVersion extends true + ? TransactionId + : TransactionId | undefined { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return this.metadata[TRANSACTION_ID_FIELD] as HasVersion extends true + ? TransactionId + : TransactionId | undefined; + } + + public get schema(): Record { + return this.metadata.function_data.__schema; + } + + public get data(): T { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return (this.metadata.function_data.data ?? {}) as T; + } + + static { + serializeParametersForCache = (parameters) => ({ + metadata: parameters.metadata, + }); + } + + public validate(data: unknown): boolean { + if (typeof data !== "object" || data === null) { + return false; + } + const schemaProps = this.schema.properties; + if (typeof schemaProps !== "object" || schemaProps === null) { + return true; + } + for (const key of Object.keys(schemaProps)) { + if (!(key in data)) { + const required = Array.isArray(this.schema.required) + ? this.schema.required + : []; + if (required.includes(key)) { + return false; + } + } + } + return true; + } + + public static isParameters( + x: unknown, + ): x is RemoteEvalParameters> { + return ( + typeof x === "object" && + x !== null && + "__braintrust_parameters_marker" in x && + x.__braintrust_parameters_marker === true + ); + } +} + +/** + * Get the versions for a prompt. + * + * @param projectId The ID of the project to query + * @param promptId The ID of the prompt to get versions for + * @returns Promise containing the version data + */ +export async function getPromptVersions( + projectId: string, + promptId: string, +): Promise { + const state = _internalGetGlobalState(); + if (!state) { + throw new Error("Must log in first"); + } + + await state.login({}); + + const query = { + from: { + op: "function", + name: { + op: "ident", + name: ["project_prompts"], + }, + args: [ + { + op: "literal", + value: projectId, + }, + ], + }, + select: [ + { + op: "star", + }, + ], + filter: { + op: "eq", + left: { op: "ident", name: ["id"] }, + right: { op: "literal", value: promptId }, + }, + }; + + const response = await state.apiConn().post( + "btql", + { + query, + audit_log: true, + use_columnstore: false, + brainstore_realtime: true, + }, + { headers: { "Accept-Encoding": "gzip" } }, + BTQL_HTTP_RETRIES, + ); + + if (!response.ok) { + throw new Error( + `API request failed: ${response.status} ${response.statusText}`, + ); + } + + const result = await response.json(); + + // Filter for entries where audit_data.action is "upsert" or "merge" and return only _xact_id fields + return ( + result.data + ?.filter((entry: any) => + ["upsert", "merge"].includes(entry.audit_data?.action), + ) + .map((entry: any) => prettifyXact(entry._xact_id)) || [] + ); +} + +interface LoaderCacheViews { + promptCache: PromptCache; + parametersCache: ParametersCache; +} + +const loaderCaches = /* @__PURE__ */ new WeakMap< + BraintrustState, + LoaderCacheViews +>(); + +/** @internal Caches belong to prompt/parameter loading, not tracing state. */ +export function getLoaderCaches(state: BraintrustState): LoaderCacheViews { + let caches = loaderCaches.get(state); + if (!caches) { + const { memoryCache, diskCache } = createCacheLayers< + PromptMemoryCacheEntry, + PromptDiskCacheEntry + >({ + memoryMaxEnvVar: "BRAINTRUST_PROMPT_CACHE_MEMORY_MAX", + diskCacheDirEnvVar: "BRAINTRUST_PROMPT_CACHE_DIR", + diskMaxEnvVar: "BRAINTRUST_PROMPT_CACHE_DISK_MAX", + getDefaultDiskCacheDir: () => + `${iso.getEnv("HOME") ?? iso.homedir!()}/.braintrust/prompt_cache`, + }); + const promptCache = new PromptCache({ memoryCache, diskCache }); + + const { + memoryCache: parametersMemoryCache, + diskCache: parametersDiskCache, + } = createCacheLayers( + { + memoryMaxEnvVar: "BRAINTRUST_PARAMETERS_CACHE_MEMORY_MAX", + diskCacheDirEnvVar: "BRAINTRUST_PARAMETERS_CACHE_DIR", + diskMaxEnvVar: "BRAINTRUST_PARAMETERS_CACHE_DISK_MAX", + getDefaultDiskCacheDir: () => + `${iso.getEnv("HOME") ?? iso.homedir!()}/.braintrust/parameters_cache`, + }, + ); + const parametersCache = new ParametersCache({ + memoryCache: parametersMemoryCache, + diskCache: parametersDiskCache, + }); + + caches = { promptCache, parametersCache }; + loaderCaches.set(state, caches); + } + return caches; +} + +function getLoaderCacheViews( + state: BraintrustState, + loginOptions: ResolvedLoaderLoginOptions, + requestState?: LoaderRequestState, +): LoaderCacheViews { + const caches = getLoaderCaches(state); + + const expectedResolvedOrgIdentity = + requestState?.orgId && requestState.appUrl + ? JSON.stringify(["loader-org", requestState.appUrl, requestState.orgId]) + : undefined; + + return { + promptCache: caches.promptCache.withNamespace( + loginOptions.credentialCacheNamespace, + expectedResolvedOrgIdentity, + ), + parametersCache: caches.parametersCache.withNamespace( + loginOptions.credentialCacheNamespace, + expectedResolvedOrgIdentity, + ), + }; +} diff --git a/js/src/logger/testing.ts b/js/src/logger/testing.ts new file mode 100644 index 000000000..416f0e6b4 --- /dev/null +++ b/js/src/logger/testing.ts @@ -0,0 +1,138 @@ +import { + extractAttachments, + deepCopyEvent, + isGeneratorFunction, + isAsyncGeneratorFunction, + validateTags, +} from "./core"; +import { resetDebugLoggerForTests } from "../debug-logger"; +/// +import iso from "#braintrust-platform"; +import { LazyValue } from "../util"; +import { + TestBackgroundLogger, + type BraintrustState, + _internalGetGlobalState, + ProjectExperimentMetadata, + _internalSetInitialState, + login, + RESET_CONTEXT_MANAGER_STATE, +} from "./core"; +import { Experiment } from "./experiments"; + +// Return a TestBackgroundLogger that will intercept logs before they are sent to the server. +// Used for testing only. +function useTestBackgroundLogger(): TestBackgroundLogger { + const state = _internalGetGlobalState(); + if (!state) { + throw new Error("global state not set yet"); + } + + const logger = new TestBackgroundLogger(); + state.setOverrideBgLogger(logger); + return logger; +} + +function clearTestBackgroundLogger(): void { + _internalGetGlobalState()?.setOverrideBgLogger(null); +} + +// Initialize a test Experiment without making network calls by injecting fake metadata. +// Useful for unit tests that need an Experiment instance. +function initTestExperiment( + experimentName: string, + projectName?: string, + experimentFullInfo: Record = {}, +): Experiment { + setInitialTestState(); + const state = _internalGetGlobalState(); + const project = projectName ?? experimentName; + + const lazyMetadata: LazyValue = new LazyValue( + async () => ({ + project: { id: project, name: project, fullInfo: {} }, + experiment: { + id: experimentName, + name: experimentName, + fullInfo: experimentFullInfo, + }, + }), + ); + + return new Experiment(state, lazyMetadata); +} + +/** + * Allows accessing helper functions for testing. + * @internal + */ + +// it's used to circumvent login for testing, but won't actually work +// on the server side. +const TEST_API_KEY = "___TEST_API_KEY__THIS_IS_NOT_REAL___"; + +function setInitialTestState(): void { + // a way of setting initial state for tests without any log messages + if (!_internalGetGlobalState()) { + _internalSetInitialState(); + } +} + +// This is a helper function to simulate a login for testing. +async function simulateLoginForTests(): Promise { + return await login({ + apiKey: TEST_API_KEY, + appUrl: "https://braintrust.dev", + }); +} + +// This is a helper function to simulate a logout for testing. +function simulateLogoutForTests(): BraintrustState { + const state = _internalGetGlobalState(); + state.resetLoginInfo(); + state.resetIdGenState(); + state[RESET_CONTEXT_MANAGER_STATE](); + state.appUrl = "https://www.braintrust.dev"; + return state; +} + +// Helper function to reset ID generator state for testing +function resetIdGenStateForTests(): void { + const state = _internalGetGlobalState(); + if (state) { + state.resetIdGenState(); + } +} + +export const _exportsForTestingOnly: { + extractAttachments: typeof extractAttachments; + deepCopyEvent: typeof deepCopyEvent; + useTestBackgroundLogger: typeof useTestBackgroundLogger; + clearTestBackgroundLogger: typeof clearTestBackgroundLogger; + resetDebugLoggerForTests: typeof resetDebugLoggerForTests; + simulateLoginForTests: typeof simulateLoginForTests; + simulateLogoutForTests: typeof simulateLogoutForTests; + setInitialTestState: typeof setInitialTestState; + initTestExperiment: typeof initTestExperiment; + isGeneratorFunction: typeof isGeneratorFunction; + isAsyncGeneratorFunction: typeof isAsyncGeneratorFunction; + resetIdGenStateForTests: typeof resetIdGenStateForTests; + validateTags: typeof validateTags; + isomorph: any; +} = { + extractAttachments, + deepCopyEvent, + useTestBackgroundLogger, + clearTestBackgroundLogger, + resetDebugLoggerForTests, + simulateLoginForTests, + simulateLogoutForTests, + setInitialTestState, + initTestExperiment, + isGeneratorFunction, + isAsyncGeneratorFunction, + resetIdGenStateForTests, + validateTags, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + isomorph: iso as any, // Expose isomorph for build type detection +}; diff --git a/js/src/lru-cache.test.ts b/js/src/lru-cache.test.ts index 06235373d..e8a907b05 100644 --- a/js/src/lru-cache.test.ts +++ b/js/src/lru-cache.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { LRUCache } from "./lru-cache"; describe("LRUCache", () => { diff --git a/js/src/lru-cache.ts b/js/src/lru-cache.ts index a71a7a2e5..f22e9982e 100644 --- a/js/src/lru-cache.ts +++ b/js/src/lru-cache.ts @@ -72,7 +72,7 @@ export class LRUCache { this.cache.delete(key); } else if (this.maxSize && this.cache.size >= this.maxSize) { // Map.keys() iterates in insertion order. - const first = this.cache.keys().next().value; + const first = this.cache.keys().next().value!; this.cache.delete(first); } this.cache.set(key, { value }); diff --git a/js/src/masking.test.ts b/js/src/masking.test.ts index 11e64d88e..08350f82c 100644 --- a/js/src/masking.test.ts +++ b/js/src/masking.test.ts @@ -1,11 +1,18 @@ -import { expect, test, describe, beforeEach, afterEach, vi } from "vitest"; import { - _exportsForTestingOnly, + expect, + test, + describe, + beforeEach, + afterEach, + vi, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "./logger/testing"; +import { initLogger, wrapTraced, currentSpan, setMaskingFunction, -} from "./logger"; +} from "./logger/core"; import { configureNode } from "./node/config"; import { setSpanCustomizers } from "./span-customizer"; import { diff --git a/js/src/node/apply-auto-instrumentation-entry.ts b/js/src/node/apply-auto-instrumentation-entry.ts index cd199bba4..4ee600fa9 100644 --- a/js/src/node/apply-auto-instrumentation-entry.ts +++ b/js/src/node/apply-auto-instrumentation-entry.ts @@ -1,3 +1,4 @@ +import "../auto"; import { applyAutoInstrumentation } from "./apply-auto-instrumentation"; applyAutoInstrumentation(); diff --git a/js/src/node/apply-auto-instrumentation.ts b/js/src/node/apply-auto-instrumentation.ts index 07dad6a61..80fdec0e6 100644 --- a/js/src/node/apply-auto-instrumentation.ts +++ b/js/src/node/apply-auto-instrumentation.ts @@ -1,5 +1,4 @@ import { register } from "node:module"; -import { pathToFileURL } from "node:url"; import { getDefaultAutoInstrumentationConfigs } from "../auto-instrumentations/configs/all"; import { ModulePatch } from "../auto-instrumentations/loader/cjs-patch"; import { debugLogger } from "../debug-logger"; @@ -37,10 +36,9 @@ export function applyAutoInstrumentation(): void { } const allConfigs = getDefaultAutoInstrumentationConfigs(); - const currentModuleUrl = getCurrentModuleUrl(); - register("./auto-instrumentations/loader/esm-hook.mjs", { - parentURL: currentModuleUrl, + register("../../hooks/loader/esm-hook.mjs", { + parentURL: import.meta.url, data: { instrumentations: allConfigs }, }); state.applied = true; @@ -54,18 +52,3 @@ export function applyAutoInstrumentation(): void { ); } } - -function getCurrentModuleUrl(): string { - if (typeof __filename !== "undefined") { - return pathToFileURL(__filename).href; - } - - const stack = new Error().stack ?? ""; - const match = - stack.match(/\((file:\/\/[^)]+)\)/) ?? stack.match(/\s(file:\/\/\S+)/); - if (match) { - return match[1].replace(/:\d+:\d+$/, ""); - } - - return pathToFileURL(process.argv[1] ?? process.cwd()).href; -} diff --git a/js/src/node/config.test.ts b/js/src/node/config.test.ts index 97ed25839..c6d001a30 100644 --- a/js/src/node/config.test.ts +++ b/js/src/node/config.test.ts @@ -1,9 +1,9 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import iso from "../isomorph"; +import iso from "#braintrust-platform"; import { configureNode } from "./config"; describe("configureNode .env.braintrust API key lookup", () => { diff --git a/js/src/node/config.ts b/js/src/node/config.ts index 6b3f37802..9ef579f53 100644 --- a/js/src/node/config.ts +++ b/js/src/node/config.ts @@ -1,193 +1,7 @@ -import { AsyncLocalStorage } from "node:async_hooks"; -import * as path from "node:path"; -import * as fs from "node:fs/promises"; -import * as os from "node:os"; -import * as fsSync from "node:fs"; -import * as crypto from "node:crypto"; -import { promisify } from "node:util"; -import * as zlib from "node:zlib"; -import * as dotenv from "dotenv"; +import iso from "#braintrust-platform"; +import { createNodeIsomorph } from "./platform"; -import iso from "../isomorph"; -import { getRepoInfo, getPastNAncestors } from "../gitutil"; -import { getCallerLocation } from "../stackutil"; -import { _internalSetInitialState } from "../logger"; -import { registry } from "../instrumentation/registry"; -import { - isInstrumentationIntegrationDisabled, - readDisabledInstrumentationEnvConfig, -} from "../instrumentation/config"; -import { installMastraExporterFactory } from "../auto-instrumentations/loader/mastra-observability-patch"; -import { BraintrustObservabilityExporter } from "../wrappers/mastra"; - -const BRAINTRUST_ENV_SEARCH_PARENT_LIMIT = 64; - -export function configureNode() { - iso.buildType = "node"; - - iso.getRepoInfo = getRepoInfo; - iso.getPastNAncestors = getPastNAncestors; - iso.getEnv = (name) => { - const value = process.env[name]; - if (name === "BRAINTRUST_API_KEY") { - return value?.trim() ? value : undefined; - } - if ( - (name === "BRAINTRUST_ENVIRONMENT_TYPE" || - name === "BRAINTRUST_ENVIRONMENT_NAME") && - !value?.trim() - ) { - return getNearestBraintrustEnvValue(name); - } - return value; - }; - iso.getBraintrustApiKey = async () => { - const value = process.env.BRAINTRUST_API_KEY; - if (value?.trim()) { - return value; - } - - // Kick off the cwd/parent reads together, then drain them nearest-first so - // a slower local file still beats a faster parent. - const envPaths = []; - for ( - let dir = process.cwd(), depth = 0; - depth <= BRAINTRUST_ENV_SEARCH_PARENT_LIMIT; - dir = path.dirname(dir), depth++ - ) { - envPaths.push(path.join(dir, ".env.braintrust")); - if (path.dirname(dir) === dir) { - break; - } - } - - type ReadResult = - | { envPath: string; index: number; contents: string } - | { envPath: string; index: number; error: unknown }; - - const pending = new Map>(); - envPaths.forEach((envPath, index) => { - pending.set( - index, - fs.readFile(envPath, "utf8").then( - (contents) => ({ contents, envPath, index }), - (error) => ({ error, envPath, index }), - ), - ); - }); - - const results: Array = []; - let nearestUnresolvedIndex = 0; - while (pending.size > 0) { - const result = await Promise.race(pending.values()); - pending.delete(result.index); - results[result.index] = result; - - while (results[nearestUnresolvedIndex]) { - const nearestResult = results[nearestUnresolvedIndex]!; - if ("contents" in nearestResult) { - const parsed = dotenv.parse(nearestResult.contents); - const apiKey = parsed.BRAINTRUST_API_KEY; - return apiKey?.trim() ? apiKey : undefined; - } - - const e = nearestResult.error; - if ( - typeof e === "object" && - e !== null && - "code" in e && - e.code === "ENOENT" - ) { - nearestUnresolvedIndex++; - continue; - } - return undefined; - } - } - - return undefined; - }; - iso.getCallerLocation = getCallerLocation; - iso.newAsyncLocalStorage = () => new AsyncLocalStorage(); - iso.processOn = (event: string, handler: (code: unknown) => void) => { - process.on(event, handler); - }; - iso.basename = path.basename; - iso.writeln = (text: string) => process.stdout.write(text + "\n"); - iso.pathJoin = path.join; - iso.pathDirname = path.dirname; - iso.mkdir = fs.mkdir; - iso.writeFile = fs.writeFile; - iso.readFile = fs.readFile; - iso.readdir = fs.readdir; - iso.stat = fs.stat; - iso.statSync = fsSync.statSync; - iso.utimes = fs.utimes; - iso.unlink = fs.unlink; - iso.homedir = os.homedir; - iso.tmpdir = os.tmpdir; - iso.writeFileSync = fsSync.writeFileSync; - iso.appendFileSync = fsSync.appendFileSync; - iso.readFileSync = (filename: string, encoding: string) => - fsSync.readFileSync(filename, encoding as BufferEncoding); - iso.unlinkSync = fsSync.unlinkSync; - iso.openFile = fs.open; - iso.gzip = promisify(zlib.gzip); - iso.gunzip = promisify(zlib.gunzip); - iso.hash = (data) => crypto.createHash("sha256").update(data).digest("hex"); - iso.hmacSha256 = (key, data) => - crypto.createHmac("sha256", key).update(data).digest("hex"); - iso.timingSafeEqual = (left, right) => { - const leftBuffer = Buffer.from(left); - const rightBuffer = Buffer.from(right); - return ( - leftBuffer.length === rightBuffer.length && - crypto.timingSafeEqual(leftBuffer, rightBuffer) - ); - }; - _internalSetInitialState(); - - // The Mastra patches rewritten by the bundler plugin and the loader hook - // both look up an exporter factory on `globalThis` at runtime. Register it - // here too so a bundled app (where neither hook.mjs nor the loader runs - // against `@mastra/core`) still gets the exporter installed when its code - // imports `braintrust`. The loader path (hook.mjs) also calls this; the - // `??=` makes double-registration a no-op. - const disabled = readDisabledInstrumentationEnvConfig( - iso.getEnv("BRAINTRUST_DISABLE_INSTRUMENTATION"), - ).integrations; - if (!isInstrumentationIntegrationDisabled(disabled, "mastra")) { - installMastraExporterFactory(() => new BraintrustObservabilityExporter()); - } - - // Enable auto-instrumentation - registry.enable(); -} - -function getNearestBraintrustEnvValue(name: string): string | undefined { - for ( - let dir = process.cwd(), depth = 0; - depth <= BRAINTRUST_ENV_SEARCH_PARENT_LIMIT; - dir = path.dirname(dir), depth++ - ) { - const envPath = path.join(dir, ".env.braintrust"); - try { - const parsed = dotenv.parse(fsSync.readFileSync(envPath, "utf8")); - const value = parsed[name]; - return value?.trim() ? value : undefined; - } catch (e) { - if ( - typeof e !== "object" || - e === null || - !("code" in e) || - e.code !== "ENOENT" - ) { - return undefined; - } - } - if (path.dirname(dir) === dir) { - break; - } - } - return undefined; +/** @internal Configure source-level tests and compatibility integrations. */ +export function configureNode(): void { + Object.assign(iso, createNodeIsomorph()); } diff --git a/js/src/node/index.ts b/js/src/node/index.ts index 7cf4ad1b1..d4017cad6 100644 --- a/js/src/node/index.ts +++ b/js/src/node/index.ts @@ -52,8 +52,4 @@ * @module braintrust */ -import { configureNode } from "./config"; - -configureNode(); - export * from "../exports"; diff --git a/js/src/node/isomorph.ts b/js/src/node/isomorph.ts new file mode 100644 index 000000000..bab5fc101 --- /dev/null +++ b/js/src/node/isomorph.ts @@ -0,0 +1,10 @@ +import type { Common } from "../isomorph-base"; +export type { + CallerLocation, + IsoAsyncLocalStorage, + IsoTracingChannel, + IsoChannelHandlers, +} from "../isomorph-base"; +import { createNodeIsomorph } from "./platform"; +const iso: Common = /* @__PURE__ */ createNodeIsomorph(); +export default iso; diff --git a/js/src/node/platform.ts b/js/src/node/platform.ts new file mode 100644 index 000000000..5758ea779 --- /dev/null +++ b/js/src/node/platform.ts @@ -0,0 +1,169 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import * as path from "node:path"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as fsSync from "node:fs"; +import * as crypto from "node:crypto"; +import { promisify } from "node:util"; +import * as zlib from "node:zlib"; +import * as dotenv from "dotenv"; + +import { createIsomorph, type Common } from "../isomorph-base"; +import { getRepoInfo, getPastNAncestors } from "../gitutil"; +import { getCallerLocation } from "../stackutil"; +const BRAINTRUST_ENV_SEARCH_PARENT_LIMIT = 64; + +export function createNodeIsomorph(): Common { + const iso = createIsomorph(); + iso.buildType = "node"; + + iso.getRepoInfo = getRepoInfo; + iso.getPastNAncestors = getPastNAncestors; + iso.getEnv = (name) => { + const value = process.env[name]; + if (name === "BRAINTRUST_API_KEY") { + return value?.trim() ? value : undefined; + } + if ( + (name === "BRAINTRUST_ENVIRONMENT_TYPE" || + name === "BRAINTRUST_ENVIRONMENT_NAME") && + !value?.trim() + ) { + return getNearestBraintrustEnvValue(name); + } + return value; + }; + iso.getBraintrustApiKey = async () => { + const value = process.env.BRAINTRUST_API_KEY; + if (value?.trim()) { + return value; + } + + // Kick off the cwd/parent reads together, then drain them nearest-first so + // a slower local file still beats a faster parent. + const envPaths = []; + for ( + let dir = process.cwd(), depth = 0; + depth <= BRAINTRUST_ENV_SEARCH_PARENT_LIMIT; + dir = path.dirname(dir), depth++ + ) { + envPaths.push(path.join(dir, ".env.braintrust")); + if (path.dirname(dir) === dir) { + break; + } + } + + type ReadResult = + | { envPath: string; index: number; contents: string } + | { envPath: string; index: number; error: unknown }; + + const pending = new Map>(); + envPaths.forEach((envPath, index) => { + pending.set( + index, + fs.readFile(envPath, "utf8").then( + (contents) => ({ contents, envPath, index }), + (error) => ({ error, envPath, index }), + ), + ); + }); + + const results: Array = []; + let nearestUnresolvedIndex = 0; + while (pending.size > 0) { + const result = await Promise.race(pending.values()); + pending.delete(result.index); + results[result.index] = result; + + while (results[nearestUnresolvedIndex]) { + const nearestResult = results[nearestUnresolvedIndex]!; + if ("contents" in nearestResult) { + const parsed = dotenv.parse(nearestResult.contents); + const apiKey = parsed.BRAINTRUST_API_KEY; + return apiKey?.trim() ? apiKey : undefined; + } + + const e = nearestResult.error; + if ( + typeof e === "object" && + e !== null && + "code" in e && + e.code === "ENOENT" + ) { + nearestUnresolvedIndex++; + continue; + } + return undefined; + } + } + + return undefined; + }; + iso.getCallerLocation = getCallerLocation; + iso.newAsyncLocalStorage = () => new AsyncLocalStorage(); + iso.processOn = (event: string, handler: (code: unknown) => void) => { + process.on(event, handler); + }; + iso.basename = path.basename; + iso.writeln = (text: string) => process.stdout.write(text + "\n"); + iso.pathJoin = path.join; + iso.pathDirname = path.dirname; + iso.mkdir = fs.mkdir; + iso.writeFile = fs.writeFile; + iso.readFile = fs.readFile; + iso.readdir = fs.readdir; + iso.stat = fs.stat; + iso.statSync = fsSync.statSync; + iso.utimes = fs.utimes; + iso.unlink = fs.unlink; + iso.homedir = os.homedir; + iso.tmpdir = os.tmpdir; + iso.writeFileSync = fsSync.writeFileSync; + iso.appendFileSync = fsSync.appendFileSync; + iso.readFileSync = (filename: string, encoding: string) => + fsSync.readFileSync(filename, encoding as BufferEncoding); + iso.unlinkSync = fsSync.unlinkSync; + iso.openFile = fs.open; + iso.gzip = promisify(zlib.gzip); + iso.gunzip = promisify(zlib.gunzip); + iso.hash = (data) => crypto.createHash("sha256").update(data).digest("hex"); + iso.hmacSha256 = (key, data) => + crypto.createHmac("sha256", key).update(data).digest("hex"); + iso.timingSafeEqual = (left, right) => { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + return ( + leftBuffer.length === rightBuffer.length && + crypto.timingSafeEqual(leftBuffer, rightBuffer) + ); + }; + return iso; +} + +function getNearestBraintrustEnvValue(name: string): string | undefined { + for ( + let dir = process.cwd(), depth = 0; + depth <= BRAINTRUST_ENV_SEARCH_PARENT_LIMIT; + dir = path.dirname(dir), depth++ + ) { + const envPath = path.join(dir, ".env.braintrust"); + try { + const parsed = dotenv.parse(fsSync.readFileSync(envPath, "utf8")); + const value = parsed[name]; + return value?.trim() ? value : undefined; + } catch (e) { + if ( + typeof e !== "object" || + e === null || + !("code" in e) || + e.code !== "ENOENT" + ) { + return undefined; + } + } + if (path.dirname(dir) === dir) { + break; + } + } + return undefined; +} diff --git a/js/src/object-fetcher.test.ts b/js/src/object-fetcher.test.ts index 5c6936cdd..be1efbef6 100644 --- a/js/src/object-fetcher.test.ts +++ b/js/src/object-fetcher.test.ts @@ -1,9 +1,6 @@ -import { describe, expect, test, vi } from "vitest"; -import { - DEFAULT_FETCH_BATCH_SIZE, - ObjectFetcher, - type BraintrustState, -} from "./logger"; +import { describe, expect, test, vi } from "vite-plus/test"; +import { DEFAULT_FETCH_BATCH_SIZE, ObjectFetcher } from "./logger/objects"; +import { type BraintrustState } from "./logger/core"; import { configureNode } from "./node/config"; configureNode(); diff --git a/js/src/openai-agents-api.ts b/js/src/openai-agents-api.ts index b461ae020..2ef397cb9 100644 --- a/js/src/openai-agents-api.ts +++ b/js/src/openai-agents-api.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "./instrumentation/registrations/openai"; import { debugLogger } from "./debug-logger"; import { openAIChannels } from "./instrumentation/providers/openai-channels"; import { resolveUseLegacyUuidIds } from "./id-gen"; @@ -5,7 +6,7 @@ import { _internalExportParentSynchronously, getSpanParentObject, newId, -} from "./logger"; +} from "./logger/core"; import { getCurrentUnixTimestamp } from "./util"; import { SpanComponentsV4 } from "../util/span_identifier_v4"; import type { @@ -127,6 +128,7 @@ export function startOpenAIAgentsTrace( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Provider types vary between OpenAI SDK versions; validate the narrow surface at runtime. args: any, ): OpenAIAgentsTraceToken { + registerInstrumentation(); const validatedArgs = validateStartArgs(args); const state = createTraceState(); if (!state) { @@ -167,6 +169,7 @@ export async function updateOpenAIAgentsTrace( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Provider event types vary between OpenAI SDK versions; validate the narrow surface at runtime. event: any, ): Promise { + registerInstrumentation(); const state = token; const validatedEvent = validateEvent(event); try { @@ -195,6 +198,7 @@ export async function failOpenAIAgentsTrace( token: OpenAIAgentsTraceToken, error: unknown, ): Promise { + registerInstrumentation(); const state = token; try { return await openAIChannels.agentsTraceFail.invoke( diff --git a/js/src/openai-batch.ts b/js/src/openai-batch.ts index 875881010..8460008e8 100644 --- a/js/src/openai-batch.ts +++ b/js/src/openai-batch.ts @@ -1,5 +1,6 @@ +import { registerInstrumentation } from "./instrumentation/registrations/openai"; import { openAIChannels } from "./instrumentation/providers/openai-channels"; -import { getSpanParentObject } from "./logger"; +import { getSpanParentObject } from "./logger/core"; import { createLazyAPIPromise, type EnhancedResponse, @@ -126,6 +127,7 @@ export function openaiFilesCreateTraced< >( files: OpenAIFilesResource, ): (params: TParams, options?: TOptions) => OpenAIAPIPromise { + registerInstrumentation(); return (params, options) => { const parent = getSpanParentObject(); const purpose = read(params, "purpose"); @@ -197,6 +199,7 @@ export function openaiBatchesRetrieveTraced< >( batches: OpenAIBatchesResource, ): (batchId: string, options?: TOptions) => OpenAIAPIPromise { + registerInstrumentation(); return (batchId, options) => { const apiPromise = batches.retrieve(batchId, options); let enhancedResponse: EnhancedResponse | undefined; @@ -255,6 +258,7 @@ export function openaiBatchesRetrieveTraced< export async function completeOpenAIBatchTrace( args: CompleteOpenAIBatchTraceArgs, ): Promise { + registerInstrumentation(); const inputFileContent = Promise.resolve(args.inputFileContent); const outputFileContent = args.outputFileContent === undefined diff --git a/js/src/openai-promise-utils.test.ts b/js/src/openai-promise-utils.test.ts index 461cbc6fc..9c8d864c5 100644 --- a/js/src/openai-promise-utils.test.ts +++ b/js/src/openai-promise-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { openAIChannels } from "./instrumentation/providers/openai-channels"; import { wrapOpenAI } from "./wrappers/oai"; import { diff --git a/js/src/package-exports.test.ts b/js/src/package-exports.test.ts index 6473e8ca1..8e6e6622e 100644 --- a/js/src/package-exports.test.ts +++ b/js/src/package-exports.test.ts @@ -1,5 +1,5 @@ import { readFileSync } from "node:fs"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "vite-plus/test"; const packageJson = JSON.parse( readFileSync(new URL("../package.json", import.meta.url), "utf8"), @@ -7,9 +7,11 @@ const packageJson = JSON.parse( describe("package exports", () => { test("publishes the curated entrypoints", () => { - expect(Object.keys(packageJson.exports).sort()).toEqual( + const keys = Object.keys(packageJson.exports); + expect(keys.sort()).toEqual( [ ".", + "./auto", "./apply-auto-instrumentation", "./custom-views", "./preprocessors", diff --git a/js/src/parameters.test.ts b/js/src/parameters.test.ts index 79cca4da8..e72761ad2 100644 --- a/js/src/parameters.test.ts +++ b/js/src/parameters.test.ts @@ -1,7 +1,7 @@ -import { expect, expectTypeOf, test, beforeAll } from "vitest"; +import { expect, expectTypeOf, test, beforeAll } from "vite-plus/test"; import { type InferParameters, validateParameters } from "./eval-parameters"; import { Eval, runEvaluator } from "./framework"; -import { RemoteEvalParameters } from "./logger"; +import { RemoteEvalParameters } from "./logger/prompts"; import { z } from "zod/v3"; import * as z4 from "zod/v4"; import { type ProgressReporter } from "./reporters/types"; diff --git a/js/src/preprocessors/registry.test.ts b/js/src/preprocessors/registry.test.ts index ae15c7267..322e84a42 100644 --- a/js/src/preprocessors/registry.test.ts +++ b/js/src/preprocessors/registry.test.ts @@ -1,4 +1,4 @@ -import { expect, expectTypeOf, test, vi } from "vitest"; +import { expect, expectTypeOf, test, vi } from "vite-plus/test"; import { customPreprocessor, type CustomPreprocessor, diff --git a/js/src/prompt-cache/cache-config.ts b/js/src/prompt-cache/cache-config.ts index 66dcc5e87..b06fc748c 100644 --- a/js/src/prompt-cache/cache-config.ts +++ b/js/src/prompt-cache/cache-config.ts @@ -1,5 +1,5 @@ import { debugLogger } from "../debug-logger"; -import iso from "../isomorph"; +import iso from "#braintrust-platform"; import { canUseDiskCache, DiskCache } from "./disk-cache"; import { LRUCache } from "../lru-cache"; diff --git a/js/src/prompt-cache/cache-mode.test.ts b/js/src/prompt-cache/cache-mode.test.ts index 0a585b0c2..f7549f66a 100644 --- a/js/src/prompt-cache/cache-mode.test.ts +++ b/js/src/prompt-cache/cache-mode.test.ts @@ -1,10 +1,19 @@ +import { getLoaderCaches } from "../logger/prompts"; import * as fs from "fs/promises"; import * as path from "path"; import { tmpdir } from "os"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import iso from "../isomorph"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; +import iso from "#braintrust-platform"; import { configureNode } from "../node/config"; -import { BraintrustState, Prompt, RemoteEvalParameters } from "../logger"; +import { BraintrustState } from "../logger/core"; +import { Prompt, RemoteEvalParameters } from "../logger/prompts"; configureNode(); @@ -117,19 +126,27 @@ describe("prompt and parameters cache modes", () => { await configureCacheDirs(); const state = new BraintrustState({}); - await state.promptCache.set(promptKey, testPrompt); - await state.promptCache.set({ ...promptKey, slug: "other" }, testPrompt); - await state.parametersCache.set(parametersKey, testParameters); - await state.parametersCache.set( + await getLoaderCaches(state).promptCache.set(promptKey, testPrompt); + await getLoaderCaches(state).promptCache.set( + { ...promptKey, slug: "other" }, + testPrompt, + ); + await getLoaderCaches(state).parametersCache.set( + parametersKey, + testParameters, + ); + await getLoaderCaches(state).parametersCache.set( { ...parametersKey, slug: "other" }, testParameters, ); const nextState = new BraintrustState({}); - expect(await nextState.promptCache.get(promptKey)).toEqual(testPrompt); - expect(await nextState.parametersCache.get(parametersKey)).toEqual( - testParameters, + expect(await getLoaderCaches(nextState).promptCache.get(promptKey)).toEqual( + testPrompt, ); + expect( + await getLoaderCaches(nextState).parametersCache.get(parametersKey), + ).toEqual(testParameters); }); it("memory mode does not write to disk", async () => { @@ -137,19 +154,28 @@ describe("prompt and parameters cache modes", () => { const { promptDir, parametersDir } = await configureCacheDirs(); const state = new BraintrustState({}); - await state.promptCache.set(promptKey, testPrompt); - await state.parametersCache.set(parametersKey, testParameters); - - expect(await state.promptCache.get(promptKey)).toEqual(testPrompt); - expect(await state.parametersCache.get(parametersKey)).toEqual( + await getLoaderCaches(state).promptCache.set(promptKey, testPrompt); + await getLoaderCaches(state).parametersCache.set( + parametersKey, testParameters, ); + + expect(await getLoaderCaches(state).promptCache.get(promptKey)).toEqual( + testPrompt, + ); + expect( + await getLoaderCaches(state).parametersCache.get(parametersKey), + ).toEqual(testParameters); expect(await fs.readdir(promptDir)).toEqual([]); expect(await fs.readdir(parametersDir)).toEqual([]); const nextState = new BraintrustState({}); - expect(await nextState.promptCache.get(promptKey)).toBeUndefined(); - expect(await nextState.parametersCache.get(parametersKey)).toBeUndefined(); + expect( + await getLoaderCaches(nextState).promptCache.get(promptKey), + ).toBeUndefined(); + expect( + await getLoaderCaches(nextState).parametersCache.get(parametersKey), + ).toBeUndefined(); }); it("disk mode does not warm memory", async () => { @@ -157,20 +183,29 @@ describe("prompt and parameters cache modes", () => { const { promptDir, parametersDir } = await configureCacheDirs(); const state = new BraintrustState({}); - await state.promptCache.set(promptKey, testPrompt); - await state.parametersCache.set(parametersKey, testParameters); + await getLoaderCaches(state).promptCache.set(promptKey, testPrompt); + await getLoaderCaches(state).parametersCache.set( + parametersKey, + testParameters, + ); const nextState = new BraintrustState({}); - expect(await nextState.promptCache.get(promptKey)).toEqual(testPrompt); - expect(await nextState.parametersCache.get(parametersKey)).toEqual( - testParameters, + expect(await getLoaderCaches(nextState).promptCache.get(promptKey)).toEqual( + testPrompt, ); + expect( + await getLoaderCaches(nextState).parametersCache.get(parametersKey), + ).toEqual(testParameters); await fs.rm(promptDir, { recursive: true, force: true }); await fs.rm(parametersDir, { recursive: true, force: true }); - expect(await nextState.promptCache.get(promptKey)).toBeUndefined(); - expect(await nextState.parametersCache.get(parametersKey)).toBeUndefined(); + expect( + await getLoaderCaches(nextState).promptCache.get(promptKey), + ).toBeUndefined(); + expect( + await getLoaderCaches(nextState).parametersCache.get(parametersKey), + ).toBeUndefined(); }); it("none mode disables all cache reads and writes", async () => { @@ -178,11 +213,18 @@ describe("prompt and parameters cache modes", () => { const { promptDir, parametersDir } = await configureCacheDirs(); const state = new BraintrustState({}); - await state.promptCache.set(promptKey, testPrompt); - await state.parametersCache.set(parametersKey, testParameters); + await getLoaderCaches(state).promptCache.set(promptKey, testPrompt); + await getLoaderCaches(state).parametersCache.set( + parametersKey, + testParameters, + ); - expect(await state.promptCache.get(promptKey)).toBeUndefined(); - expect(await state.parametersCache.get(parametersKey)).toBeUndefined(); + expect( + await getLoaderCaches(state).promptCache.get(promptKey), + ).toBeUndefined(); + expect( + await getLoaderCaches(state).parametersCache.get(parametersKey), + ).toBeUndefined(); expect(await fs.readdir(promptDir)).toEqual([]); expect(await fs.readdir(parametersDir)).toEqual([]); }); @@ -194,14 +236,19 @@ describe("prompt and parameters cache modes", () => { await configureCacheDirs(); const state = new BraintrustState({}); - await state.promptCache.set(promptKey, testPrompt); - await state.parametersCache.set(parametersKey, testParameters); + await getLoaderCaches(state).promptCache.set(promptKey, testPrompt); + await getLoaderCaches(state).parametersCache.set( + parametersKey, + testParameters, + ); const nextState = new BraintrustState({}); - expect(await nextState.promptCache.get(promptKey)).toEqual(testPrompt); - expect(await nextState.parametersCache.get(parametersKey)).toEqual( - testParameters, + expect(await getLoaderCaches(nextState).promptCache.get(promptKey)).toEqual( + testPrompt, ); + expect( + await getLoaderCaches(nextState).parametersCache.get(parametersKey), + ).toEqual(testParameters); expect(warnSpy).toHaveBeenCalledTimes(1); expect(warnSpy).toHaveBeenCalledWith( @@ -216,30 +263,36 @@ describe("prompt and parameters cache modes", () => { const state = new BraintrustState({}); for (let i = 0; i < 1025; i++) { - await state.promptCache.set( + await getLoaderCaches(state).promptCache.set( { ...promptKey, slug: `prompt-${i}` }, testPrompt, ); - await state.parametersCache.set( + await getLoaderCaches(state).parametersCache.set( { ...parametersKey, slug: `parameters-${i}` }, testParameters, ); } expect( - await state.promptCache.get({ ...promptKey, slug: "prompt-0" }), + await getLoaderCaches(state).promptCache.get({ + ...promptKey, + slug: "prompt-0", + }), ).toBeUndefined(); expect( - await state.parametersCache.get({ + await getLoaderCaches(state).parametersCache.get({ ...parametersKey, slug: "parameters-0", }), ).toBeUndefined(); expect( - await state.promptCache.get({ ...promptKey, slug: "prompt-1024" }), + await getLoaderCaches(state).promptCache.get({ + ...promptKey, + slug: "prompt-1024", + }), ).toEqual(testPrompt); expect( - await state.parametersCache.get({ + await getLoaderCaches(state).parametersCache.get({ ...parametersKey, slug: "parameters-1024", }), @@ -253,10 +306,17 @@ describe("prompt and parameters cache modes", () => { iso.gzip = undefined; const state = new BraintrustState({}); - await state.promptCache.set(promptKey, testPrompt); - expect(await state.promptCache.get(promptKey)).toBeUndefined(); - await state.parametersCache.set(parametersKey, testParameters); - expect(await state.parametersCache.get(parametersKey)).toBeUndefined(); + await getLoaderCaches(state).promptCache.set(promptKey, testPrompt); + expect( + await getLoaderCaches(state).promptCache.get(promptKey), + ).toBeUndefined(); + await getLoaderCaches(state).parametersCache.set( + parametersKey, + testParameters, + ); + expect( + await getLoaderCaches(state).parametersCache.get(parametersKey), + ).toBeUndefined(); new BraintrustState({}); expect(warnSpy).toHaveBeenCalledTimes(1); diff --git a/js/src/prompt-cache/disk-cache.test.ts b/js/src/prompt-cache/disk-cache.test.ts index ed0664346..eb5bf92b9 100644 --- a/js/src/prompt-cache/disk-cache.test.ts +++ b/js/src/prompt-cache/disk-cache.test.ts @@ -2,7 +2,7 @@ import * as fs from "fs/promises"; import * as path from "path"; import { DiskCache } from "./disk-cache"; import { tmpdir } from "os"; -import { beforeEach, describe, it, afterEach, expect } from "vitest"; +import { beforeEach, describe, it, afterEach, expect } from "vite-plus/test"; import { configureNode } from "../node/config"; describe("DiskCache", () => { diff --git a/js/src/prompt-cache/disk-cache.ts b/js/src/prompt-cache/disk-cache.ts index 8d5dc256c..d69734c90 100644 --- a/js/src/prompt-cache/disk-cache.ts +++ b/js/src/prompt-cache/disk-cache.ts @@ -1,4 +1,4 @@ -import iso from "../isomorph"; +import iso from "#braintrust-platform"; export function canUseDiskCache(): boolean { return !!( diff --git a/js/src/prompt-cache/parameters-cache.test.ts b/js/src/prompt-cache/parameters-cache.test.ts index 7fe320871..96596065c 100644 --- a/js/src/prompt-cache/parameters-cache.test.ts +++ b/js/src/prompt-cache/parameters-cache.test.ts @@ -1,8 +1,8 @@ import * as fs from "fs/promises"; import * as path from "path"; import { tmpdir } from "os"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { RemoteEvalParameters } from "../logger"; +import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; +import { RemoteEvalParameters } from "../logger/prompts"; import { configureNode } from "../node/config"; import { DiskCache } from "./disk-cache"; import { diff --git a/js/src/prompt-cache/parameters-cache.ts b/js/src/prompt-cache/parameters-cache.ts index 530cfb28a..a32ad58fd 100644 --- a/js/src/prompt-cache/parameters-cache.ts +++ b/js/src/prompt-cache/parameters-cache.ts @@ -2,7 +2,7 @@ import { RemoteEvalParameters, serializeParametersForCache, type SerializedParameters, -} from "../logger"; +} from "../logger/prompts"; import { LRUCache } from "../lru-cache"; import { DiskCache } from "./disk-cache"; diff --git a/js/src/prompt-cache/prompt-cache.test.ts b/js/src/prompt-cache/prompt-cache.test.ts index 5c7dbe596..ef392e363 100644 --- a/js/src/prompt-cache/prompt-cache.test.ts +++ b/js/src/prompt-cache/prompt-cache.test.ts @@ -5,9 +5,9 @@ import { type PromptDiskCacheEntry, type PromptMemoryCacheEntry, } from "./prompt-cache"; -import { Prompt } from "../logger"; +import { Prompt } from "../logger/prompts"; import { tmpdir } from "os"; -import { beforeEach, describe, it, afterEach, expect } from "vitest"; +import { beforeEach, describe, it, afterEach, expect } from "vite-plus/test"; import type { PromptKey } from "./prompt-cache"; import { DiskCache } from "./disk-cache"; import { configureNode } from "../node/config"; diff --git a/js/src/prompt-cache/prompt-cache.ts b/js/src/prompt-cache/prompt-cache.ts index 8fa7b291a..14d7083fa 100644 --- a/js/src/prompt-cache/prompt-cache.ts +++ b/js/src/prompt-cache/prompt-cache.ts @@ -2,7 +2,7 @@ import { Prompt, serializePromptForCache, type SerializedPrompt, -} from "../logger"; +} from "../logger/prompts"; import { LRUCache } from "../lru-cache"; import { DiskCache } from "./disk-cache"; diff --git a/js/src/prompt-types.test.ts b/js/src/prompt-types.test.ts index 51df00f1b..3730206bc 100644 --- a/js/src/prompt-types.test.ts +++ b/js/src/prompt-types.test.ts @@ -1,4 +1,4 @@ -import { expectTypeOf, test } from "vitest"; +import { expectTypeOf, test } from "vite-plus/test"; import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions"; import type { CompletionCreateParamsNonStreaming } from "openai/resources/completions"; import { @@ -7,7 +7,7 @@ import { type CompiledPromptParams, type DefaultPromptArgs, type PromptRowWithId, -} from "./logger"; +} from "./logger/prompts"; import type { CodeParameters, CodePrompt } from "./framework2"; import type { AnyModelParamsType, diff --git a/js/src/prompt.test.ts b/js/src/prompt.test.ts index 3e18470ff..8530c5b9b 100644 --- a/js/src/prompt.test.ts +++ b/js/src/prompt.test.ts @@ -1,6 +1,6 @@ -import { beforeAll, describe, test, expect, vi } from "vitest"; +import { beforeAll, describe, test, expect, vi } from "vite-plus/test"; import { configureNode } from "./node/config"; -import { Prompt } from "./logger"; +import { Prompt } from "./logger/prompts"; import { type PromptDataType as PromptData } from "./generated_types"; describe("prompt strict mode", () => { diff --git a/js/src/propagation.test.ts b/js/src/propagation.test.ts index e904d8847..98b4b01fb 100644 --- a/js/src/propagation.test.ts +++ b/js/src/propagation.test.ts @@ -7,16 +7,23 @@ * propagation path (no `@opentelemetry/api` dependency). */ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { - _exportsForTestingOnly, + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "./logger/testing"; +import { _internalStartSpan, _injectIntoCarrier, extractTraceContextFromHeaders, initLogger, injectTraceContext, startSpan, -} from "./logger"; +} from "./logger/core"; import { BAGGAGE_HEADER, BRAINTRUST_PARENT_KEY, diff --git a/js/src/public-types.test.ts b/js/src/public-types.test.ts index d8f5bd89a..19989155c 100644 --- a/js/src/public-types.test.ts +++ b/js/src/public-types.test.ts @@ -1,4 +1,4 @@ -import { expect, expectTypeOf, test } from "vitest"; +import { describe, expect, expectTypeOf, test } from "vite-plus/test"; import * as publicExports from "./exports"; import type { @@ -21,6 +21,7 @@ import { init, initDataset, withParent, + wrapVitest, } from "./exports"; test("exports only intentional extension types", () => { @@ -84,3 +85,11 @@ test("accepts only canonical v4 call shapes", () => { expect(true).toBe(true); }); + +test("preserves the current Vitest expect API", () => { + if (false) { + const wrapped = wrapVitest({ test, describe, expect }); + expectTypeOf(wrapped.expect).toEqualTypeOf(); + wrapped.expect(1, "named assertion").toBe(1); + } +}); diff --git a/js/src/queue.test.ts b/js/src/queue.test.ts index 32cb4b593..2a7496048 100644 --- a/js/src/queue.test.ts +++ b/js/src/queue.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { Queue } from "./queue"; test("Queue basic operations", () => { diff --git a/js/src/queue.ts b/js/src/queue.ts index 848a70bce..b43c73ec7 100644 --- a/js/src/queue.ts +++ b/js/src/queue.ts @@ -24,7 +24,7 @@ export class Queue { * Set queue size limit enforcement. When enabled, the queue will drop new items * when it reaches maxSize. When disabled (default), the queue can grow unlimited. */ - enforceQueueSizeLimit(enforce: boolean) { + enforceQueueSizeLimit(enforce: boolean): void { this.enforceSizeLimit = enforce; } diff --git a/js/src/reporters/progress.ts b/js/src/reporters/progress.ts index d1a8e5303..c0f1df2c4 100644 --- a/js/src/reporters/progress.ts +++ b/js/src/reporters/progress.ts @@ -1,11 +1,11 @@ import type { ProgressReporter } from "./types"; export class SimpleProgressReporter implements ProgressReporter { - public start(name: string, _total: number) { + public start(name: string, _total: number): void { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.log(`Running evaluator ${name}`); } - public stop() {} - public increment(_name: string) {} - public setTotal(_name: string, _total: number) {} + public stop(): void {} + public increment(_name: string): void {} + public setTotal(_name: string, _total: number): void {} } diff --git a/js/src/sandbox.ts b/js/src/sandbox.ts index 4f91fc753..678169235 100644 --- a/js/src/sandbox.ts +++ b/js/src/sandbox.ts @@ -1,7 +1,7 @@ import { z } from "zod/v3"; import { slugify } from "../util/string_util"; import type { IfExists } from "./sdk-types"; -import { type BraintrustState, _internalGetGlobalState } from "./logger"; +import { type BraintrustState, _internalGetGlobalState } from "./logger/core"; /** * Configuration for a sandbox runtime. diff --git a/js/src/sdk-contracts.test.ts b/js/src/sdk-contracts.test.ts index 00ec7cd07..4f88150ed 100644 --- a/js/src/sdk-contracts.test.ts +++ b/js/src/sdk-contracts.test.ts @@ -1,4 +1,4 @@ -import { expectTypeOf, test } from "vitest"; +import { expectTypeOf, test } from "vite-plus/test"; import type { z } from "zod/v3"; import type * as Backend from "./generated_plain_types"; import type * as backendSchemas from "./generated_types"; @@ -67,114 +67,186 @@ test("SDK-owned contracts remain compatible with the backend", () => { }); test("runtime validators accept and return compatible types", () => { - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); + expectTypeOf>().toExtend< + z.output + >(); expectTypeOf< z.input - >().toEqualTypeOf< + >().toExtend>(); + expectTypeOf< z.input - >(); + >().toExtend>(); expectTypeOf< z.output - >().toEqualTypeOf< + >().toExtend>(); + expectTypeOf< z.output - >(); - expectTypeOf>().toEqualTypeOf< - z.input - >(); - expectTypeOf>().toEqualTypeOf< - z.output - >(); - expectTypeOf>().toEqualTypeOf< + >().toExtend>(); + + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf< - z.input - >().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); expectTypeOf< - z.output - >().toEqualTypeOf< + z.input + >().toExtend>(); + expectTypeOf>().toExtend< z.output >(); expectTypeOf< - z.input - >().toEqualTypeOf>(); - expectTypeOf< - z.output - >().toEqualTypeOf>(); - expectTypeOf>().toEqualTypeOf< + z.output + >().toExtend>(); + + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.output + >(); + expectTypeOf>().toExtend< z.input >(); - expectTypeOf>().toEqualTypeOf< + expectTypeOf>().toExtend< + z.input + >(); + expectTypeOf>().toExtend< z.output >(); + expectTypeOf>().toExtend< + z.output + >(); }); diff --git a/js/src/sdk-schemas.ts b/js/src/sdk-schemas.ts index a1c8c2ef6..8186bf138 100644 --- a/js/src/sdk-schemas.ts +++ b/js/src/sdk-schemas.ts @@ -3,583 +3,658 @@ * Input/output type compatibility is checked in sdk-contracts.test.ts. */ import { z } from "zod/v3"; +import type * as SDK from "./sdk-types"; + +const ResponseFormatJsonSchema: z.ZodType = + /* @__PURE__ */ (() => + z.object({ + name: z.string(), + description: z.string().optional(), + schema: z + .union([z.object({}).partial().passthrough(), z.string()]) + .optional(), + strict: z.union([z.boolean(), z.null()]).optional(), + }))(); + +const ResponseFormatNullish = /* @__PURE__ */ (() => + z.union([ + z.object({ type: z.literal("json_object") }), + z.object({ + type: z.literal("json_schema"), + json_schema: ResponseFormatJsonSchema, + }), + z.object({ type: z.literal("text") }), + z.null(), + ]))(); -export const ResponseFormatJsonSchema = z.object({ - name: z.string(), - description: z.string().optional(), - schema: z - .union([z.object({}).partial().passthrough(), z.string()]) - .optional(), - strict: z.union([z.boolean(), z.null()]).optional(), -}); - -const ResponseFormatNullish = z.union([ - z.object({ type: z.literal("json_object") }), - z.object({ - type: z.literal("json_schema"), - json_schema: ResponseFormatJsonSchema, - }), - z.object({ type: z.literal("text") }), - z.null(), -]); - -export const BraintrustAttachmentReference = z.object({ - type: z.literal("braintrust_attachment"), - filename: z.string().min(1), - content_type: z.string().min(1), - key: z.string().min(1), -}); - -export const ExternalAttachmentReference = z.object({ - type: z.literal("external_attachment"), - filename: z.string().min(1), - content_type: z.string().min(1), - url: z.string().min(1), -}); - -export const AttachmentReference = z.discriminatedUnion("type", [ - BraintrustAttachmentReference, - ExternalAttachmentReference, -]); - -const UploadStatus = z.enum(["uploading", "done", "error"]); - -export const AttachmentStatus = z.object({ - upload_status: UploadStatus, - error_message: z.string().optional(), -}); - -const FunctionTypeEnum = z.enum([ - "llm", - "scorer", - "task", - "tool", - "custom_view", - "preprocessor", - "facet", - "classifier", - "tag", - "parameters", - "sandbox", -]); - -const SavedFunctionId = z.union([ - z.object({ - type: z.literal("function"), - id: z.string(), - version: z.string().optional(), - }), - z.object({ - type: z.literal("global"), - name: z.string(), - function_type: FunctionTypeEnum.optional().default("scorer"), - }), -]); - -export const BraintrustModelParams = z - .object({ - use_cache: z.boolean(), - reasoning_enabled: z.boolean(), - reasoning_budget: z.number(), - }) - .partial(); - -export const CallEvent = z.union([ - z.object({ - id: z.string().optional(), - data: z.string(), - event: z.literal("text_delta"), - }), - z.object({ - id: z.string().optional(), - data: z.string(), - event: z.literal("reasoning_delta"), - }), - z.object({ - id: z.string().optional(), - data: z.string(), - event: z.literal("json_delta"), - }), - z.object({ - id: z.string().optional(), - data: z.string(), - event: z.literal("progress"), - }), - z.object({ - id: z.string().optional(), - data: z.string(), - event: z.literal("error"), - }), - z.object({ - id: z.string().optional(), - data: z.string(), - event: z.literal("console"), - }), - z.object({ - id: z.string().optional(), - event: z.literal("start"), - data: z.literal(""), - }), +const BraintrustAttachmentReferenceObject = /* @__PURE__ */ (() => z.object({ - id: z.string().optional(), - event: z.literal("done"), - data: z.literal(""), - }), -]); - -const CacheControl = z.object({ - type: z.literal("ephemeral"), - ttl: z.enum(["5m", "1h"]).optional(), -}); - -const ChatCompletionContentPartText = z.object({ - text: z.string().default(""), - type: z.literal("text"), - cache_control: CacheControl.optional(), -}); - -const ChatCompletionContentPartImageWithTitle = z.object({ - image_url: z.object({ - url: z.string(), - detail: z - .union([z.literal("auto"), z.literal("low"), z.literal("high")]) - .optional(), - }), - type: z.literal("image_url"), - cache_control: CacheControl.optional(), -}); - -const ChatCompletionContentPartFileFile = z - .object({ file_data: z.string(), filename: z.string(), file_id: z.string() }) - .partial(); - -const ChatCompletionContentPartFileWithTitle = z.object({ - file: ChatCompletionContentPartFileFile, - type: z.literal("file"), - cache_control: CacheControl.optional(), -}); - -const ChatCompletionContentPartInputAudioWithTitle = z.object({ - input_audio: z.object({ data: z.string(), format: z.enum(["wav", "mp3"]) }), - type: z.literal("input_audio"), - cache_control: CacheControl.optional(), -}); -const ChatCompletionContentPart = z.union([ - ChatCompletionContentPartText, - ChatCompletionContentPartImageWithTitle, - ChatCompletionContentPartInputAudioWithTitle, - ChatCompletionContentPartFileWithTitle, -]); - -const ChatCompletionMessageToolCall = z.object({ - id: z.string(), - function: z.object({ arguments: z.string(), name: z.string() }), - type: z.literal("function"), -}); - -const ChatCompletionMessageReasoning = z - .object({ id: z.string(), content: z.string() }) - .partial(); - -const ChatCompletionMessageParam = z.union([ - z.object({ - content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), - role: z.literal("system"), - name: z.string().optional(), - }), - z.object({ - content: z.union([z.string(), z.array(ChatCompletionContentPart)]), - role: z.literal("user"), - name: z.string().optional(), - }), - z.object({ - role: z.literal("assistant"), - content: z - .union([z.string(), z.array(ChatCompletionContentPartText), z.null()]) - .optional(), - function_call: z - .object({ arguments: z.string(), name: z.string() }) - .optional(), - name: z.string().optional(), - tool_calls: z.array(ChatCompletionMessageToolCall).optional(), - reasoning: z.array(ChatCompletionMessageReasoning).optional(), - reasoning_signature: z.string().optional(), - }), - z.object({ - content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), - role: z.literal("tool"), - tool_call_id: z.string().default(""), - }), - z.object({ - content: z.union([z.string(), z.null()]), - name: z.string(), - role: z.literal("function"), - }), - z.object({ - content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), - role: z.literal("developer"), - name: z.string().optional(), - }), - z.object({ - role: z.literal("model"), - content: z.union([z.string(), z.null()]).optional(), - }), -]); + type: z.literal("braintrust_attachment"), + filename: z.string().min(1), + content_type: z.string().min(1), + key: z.string().min(1), + }))(); -export const ChatCompletionTool = z.object({ - function: z.object({ - name: z.string(), - description: z.string().optional(), - parameters: z.object({}).partial().passthrough().optional(), - }), - type: z.literal("function"), -}); - -export const ObjectReference = z.object({ - object_type: z.enum([ - "project_logs", - "experiment", - "dataset", - "prompt", - "function", - "prompt_session", - ]), - object_id: z.string().uuid(), - id: z.string(), - _xact_id: z.union([z.string(), z.null()]).optional(), - created: z.union([z.string(), z.null()]).optional(), -}); - -export const DatasetSnapshot = z.object({ - id: z.string().uuid(), - dataset_id: z.string().uuid(), - name: z.string(), - description: z.union([z.string(), z.null()]), - xact_id: z.string(), - created: z.union([z.string(), z.null()]), -}); - -const PromptBlockDataNullish = z.union([ +const ExternalAttachmentReferenceObject = /* @__PURE__ */ (() => z.object({ - type: z.literal("chat"), - messages: z.array(ChatCompletionMessageParam), - tools: z.string().optional(), - }), - z.object({ type: z.literal("completion"), content: z.string() }), - z.null(), -]); - -const ModelParams = z.union([ - BraintrustModelParams.extend({ - temperature: z.number(), - top_p: z.number(), - max_tokens: z.number(), - max_completion_tokens: z.number(), - frequency_penalty: z.number(), - presence_penalty: z.number(), - response_format: ResponseFormatNullish, - tool_choice: z.union([ - z.literal("auto"), - z.literal("none"), - z.literal("required"), - z.object({ - type: z.literal("function"), - function: z.object({ name: z.string() }), - }), - ]), - function_call: z.union([ - z.literal("auto"), - z.literal("none"), - z.object({ name: z.string() }), - ]), - n: z.number(), - stop: z.array(z.string()), - chat_template_kwargs: z - .object({ enable_thinking: z.boolean() }) - .partial() - .passthrough(), - reasoning_effort: z.enum(["none", "minimal", "low", "medium", "high"]), - verbosity: z.enum(["low", "medium", "high"]), - }) - .partial() - .passthrough(), - BraintrustModelParams.extend({ - max_tokens: z.number(), - temperature: z.number(), - top_p: z.number().optional(), - top_k: z.number().optional(), - stop_sequences: z.array(z.string()).optional(), - max_tokens_to_sample: z.number().optional(), - }).passthrough(), - BraintrustModelParams.extend({ - temperature: z.number(), - maxOutputTokens: z.number(), - topP: z.number(), - topK: z.number(), - }) - .partial() - .passthrough(), - BraintrustModelParams.extend({ - temperature: z.number(), - topK: z.number(), - }) - .partial() - .passthrough(), - BraintrustModelParams.passthrough(), -]); - -const PromptOptionsNullish = z.union([ + type: z.literal("external_attachment"), + filename: z.string().min(1), + content_type: z.string().min(1), + url: z.string().min(1), + }))(); + +export const AttachmentReference: z.ZodType = + /* @__PURE__ */ (() => + z.discriminatedUnion("type", [ + BraintrustAttachmentReferenceObject, + ExternalAttachmentReferenceObject, + ]))(); + +const UploadStatus = /* @__PURE__ */ (() => + z.enum(["uploading", "done", "error"]))(); + +export const AttachmentStatus: z.ZodType = + /* @__PURE__ */ (() => + z.object({ + upload_status: UploadStatus, + error_message: z.string().optional(), + }))(); + +const FunctionTypeEnum = /* @__PURE__ */ (() => + z.enum([ + "llm", + "scorer", + "task", + "tool", + "custom_view", + "preprocessor", + "facet", + "classifier", + "tag", + "parameters", + "sandbox", + ]))(); + +const SavedFunctionId = /* @__PURE__ */ (() => + z.union([ + z.object({ + type: z.literal("function"), + id: z.string(), + version: z.string().optional(), + }), + z.object({ + type: z.literal("global"), + name: z.string(), + function_type: FunctionTypeEnum.optional().default("scorer"), + }), + ]))(); + +const BraintrustModelParamsObject = /* @__PURE__ */ (() => z .object({ - model: z.string(), - params: ModelParams, - position: z.string(), - endpoint_name: z.union([z.string(), z.null()]), + use_cache: z.boolean(), + reasoning_enabled: z.boolean(), + reasoning_budget: z.number(), }) - .partial(), - z.null(), -]); + .partial())(); + +export const CallEvent: z.ZodType = /* @__PURE__ */ (() => + z.union([ + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("text_delta"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("reasoning_delta"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("json_delta"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("progress"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("error"), + }), + z.object({ + id: z.string().optional(), + data: z.string(), + event: z.literal("console"), + }), + z.object({ + id: z.string().optional(), + event: z.literal("start"), + data: z.literal(""), + }), + z.object({ + id: z.string().optional(), + event: z.literal("done"), + data: z.literal(""), + }), + ]))(); -const PromptParserNullish = z.union([ +const CacheControl = /* @__PURE__ */ (() => z.object({ - type: z.literal("llm_classifier"), - use_cot: z.boolean(), - choice_scores: z.record(z.number().gte(0).lte(1)).optional(), - choice: z.array(z.string()).optional(), - allow_no_match: z.boolean().optional(), - allow_skip: z.boolean().optional(), - }), - z.null(), -]); - -const PreprocessorId = z.union([ - z.object({ - type: z.literal("function"), - id: z.string(), - version: z.string().optional(), - }), + type: z.literal("ephemeral"), + ttl: z.enum(["5m", "1h"]).optional(), + }))(); + +const ChatCompletionContentPartText = /* @__PURE__ */ (() => z.object({ - type: z.literal("global"), - name: z.string(), - function_type: z.literal("preprocessor").optional().default("preprocessor"), - }), - z.object({ type: z.literal("inline"), code: z.string().min(1) }), - z.null(), -]); - -const FunctionFormat = z.enum(["llm", "code", "global", "graph", "topic_map"]); - -export const PromptData = z - .object({ - prompt: PromptBlockDataNullish, - options: PromptOptionsNullish, - parser: PromptParserNullish, - preprocessor: PreprocessorId, - tool_functions: z.union([z.array(SavedFunctionId), z.null()]), - template_format: z.union([ - z.enum(["mustache", "nunjucks", "none"]), - z.null(), - ]), - mcp: z.union([ - z.record( - z.union([ - z.object({ - type: z.literal("id"), - id: z.string().uuid(), - is_disabled: z.boolean().optional(), - enabled_tools: z.union([z.array(z.string()), z.null()]).optional(), - }), - z.object({ - type: z.literal("url"), - url: z.string(), - is_disabled: z.boolean().optional(), - enabled_tools: z.union([z.array(z.string()), z.null()]).optional(), - }), - ]), - ), - z.null(), - ]), - origin: z.union([ - z - .object({ - prompt_id: z.string(), - project_id: z.string(), - prompt_version: z.string(), - }) - .partial(), - z.null(), - ]), - }) - .partial(); - -export const FunctionId = z.union([ - z.object({ function_id: z.string(), version: z.string().optional() }), + text: z.string().default(""), + type: z.literal("text"), + cache_control: CacheControl.optional(), + }))(); + +const ChatCompletionContentPartImageWithTitle = /* @__PURE__ */ (() => z.object({ - project_name: z.string(), - slug: z.string(), - version: z.string().optional(), - }), + image_url: z.object({ + url: z.string(), + detail: z + .union([z.literal("auto"), z.literal("low"), z.literal("high")]) + .optional(), + }), + type: z.literal("image_url"), + cache_control: CacheControl.optional(), + }))(); + +const ChatCompletionContentPartFileFile = /* @__PURE__ */ (() => + z + .object({ + file_data: z.string(), + filename: z.string(), + file_id: z.string(), + }) + .partial())(); + +const ChatCompletionContentPartFileWithTitle = /* @__PURE__ */ (() => z.object({ - global_function: z.string(), - function_type: FunctionTypeEnum.optional().default("scorer"), - }), + file: ChatCompletionContentPartFileFile, + type: z.literal("file"), + cache_control: CacheControl.optional(), + }))(); + +const ChatCompletionContentPartInputAudioWithTitle = /* @__PURE__ */ (() => z.object({ - prompt_session_id: z.string(), - prompt_session_function_id: z.string(), - version: z.string().optional(), - }), + input_audio: z.object({ data: z.string(), format: z.enum(["wav", "mp3"]) }), + type: z.literal("input_audio"), + cache_control: CacheControl.optional(), + }))(); +const ChatCompletionContentPart = /* @__PURE__ */ (() => + z.union([ + ChatCompletionContentPartText, + ChatCompletionContentPartImageWithTitle, + ChatCompletionContentPartInputAudioWithTitle, + ChatCompletionContentPartFileWithTitle, + ]))(); + +const ChatCompletionMessageToolCall = /* @__PURE__ */ (() => z.object({ - inline_context: z.object({ - runtime: z.enum(["node", "python", "browser", "quickjs"]), - version: z.string(), + id: z.string(), + function: z.object({ arguments: z.string(), name: z.string() }), + type: z.literal("function"), + }))(); + +const ChatCompletionMessageReasoning = /* @__PURE__ */ (() => + z.object({ id: z.string(), content: z.string() }).partial())(); + +const ChatCompletionMessageParam = /* @__PURE__ */ (() => + z.union([ + z.object({ + content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), + role: z.literal("system"), + name: z.string().optional(), }), - code: z.string(), - function_type: FunctionTypeEnum.and(z.unknown()).optional(), - name: z.union([z.string(), z.null()]).optional(), - }), - z.object({ - inline_prompt: PromptData.optional(), - inline_function: z.object({}).partial().passthrough(), - function_type: FunctionTypeEnum.optional().default("scorer"), - name: z.union([z.string(), z.null()]).optional(), - }), - z.object({ - inline_prompt: PromptData, - function_type: FunctionTypeEnum.optional().default("scorer"), - name: z.union([z.string(), z.null()]).optional(), - }), -]); - -const FunctionObjectType = z.enum([ - "prompt", - "tool", - "scorer", - "task", - "workflow", - "custom_view", - "preprocessor", - "facet", - "classifier", - "parameters", - "sandbox", -]); - -const FunctionOutputType = z.enum([ - "completion", - "score", - "facet", - "classification", - "any", -]); - -export const GitMetadataSettings = z.object({ - collect: z.enum(["all", "none", "some"]), - fields: z - .array( - z.enum([ - "commit", - "branch", - "tag", - "dirty", - "author_name", - "author_email", - "commit_message", - "commit_time", - "git_diff", + z.object({ + content: z.union([z.string(), z.array(ChatCompletionContentPart)]), + role: z.literal("user"), + name: z.string().optional(), + }), + z.object({ + role: z.literal("assistant"), + content: z + .union([z.string(), z.array(ChatCompletionContentPartText), z.null()]) + .optional(), + function_call: z + .object({ arguments: z.string(), name: z.string() }) + .optional(), + name: z.string().optional(), + tool_calls: z.array(ChatCompletionMessageToolCall).optional(), + reasoning: z.array(ChatCompletionMessageReasoning).optional(), + reasoning_signature: z.string().optional(), + }), + z.object({ + content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), + role: z.literal("tool"), + tool_call_id: z.string().default(""), + }), + z.object({ + content: z.union([z.string(), z.null()]), + name: z.string(), + role: z.literal("function"), + }), + z.object({ + content: z.union([z.string(), z.array(ChatCompletionContentPartText)]), + role: z.literal("developer"), + name: z.string().optional(), + }), + z.object({ + role: z.literal("model"), + content: z.union([z.string(), z.null()]).optional(), + }), + ]))(); + +export const ChatCompletionTool: z.ZodType = + /* @__PURE__ */ (() => + z.object({ + function: z.object({ + name: z.string(), + description: z.string().optional(), + parameters: z.object({}).partial().passthrough().optional(), + }), + type: z.literal("function"), + }))(); + +export const ObjectReference: z.ZodType = + /* @__PURE__ */ (() => + z.object({ + object_type: z.enum([ + "project_logs", + "experiment", + "dataset", + "prompt", + "function", + "prompt_session", ]), - ) - .optional(), -}); - -const ProjectSettings = z.union([ + object_id: z.string().uuid(), + id: z.string(), + _xact_id: z.union([z.string(), z.null()]).optional(), + created: z.union([z.string(), z.null()]).optional(), + }))(); + +export const DatasetSnapshot: z.ZodType = + /* @__PURE__ */ (() => + z.object({ + id: z.string().uuid(), + dataset_id: z.string().uuid(), + name: z.string(), + description: z.union([z.string(), z.null()]), + xact_id: z.string(), + created: z.union([z.string(), z.null()]), + }))(); + +const PromptBlockDataNullish = /* @__PURE__ */ (() => + z.union([ + z.object({ + type: z.literal("chat"), + messages: z.array(ChatCompletionMessageParam), + tools: z.string().optional(), + }), + z.object({ type: z.literal("completion"), content: z.string() }), + z.null(), + ]))(); + +const ModelParams = /* @__PURE__ */ (() => + z.union([ + BraintrustModelParamsObject.extend({ + temperature: z.number(), + top_p: z.number(), + max_tokens: z.number(), + max_completion_tokens: z.number(), + frequency_penalty: z.number(), + presence_penalty: z.number(), + response_format: ResponseFormatNullish, + tool_choice: z.union([ + z.literal("auto"), + z.literal("none"), + z.literal("required"), + z.object({ + type: z.literal("function"), + function: z.object({ name: z.string() }), + }), + ]), + function_call: z.union([ + z.literal("auto"), + z.literal("none"), + z.object({ name: z.string() }), + ]), + n: z.number(), + stop: z.array(z.string()), + chat_template_kwargs: z + .object({ enable_thinking: z.boolean() }) + .partial() + .passthrough(), + reasoning_effort: z.enum(["none", "minimal", "low", "medium", "high"]), + verbosity: z.enum(["low", "medium", "high"]), + }) + .partial() + .passthrough(), + BraintrustModelParamsObject.extend({ + max_tokens: z.number(), + temperature: z.number(), + top_p: z.number().optional(), + top_k: z.number().optional(), + stop_sequences: z.array(z.string()).optional(), + max_tokens_to_sample: z.number().optional(), + }).passthrough(), + BraintrustModelParamsObject.extend({ + temperature: z.number(), + maxOutputTokens: z.number(), + topP: z.number(), + topK: z.number(), + }) + .partial() + .passthrough(), + BraintrustModelParamsObject.extend({ + temperature: z.number(), + topK: z.number(), + }) + .partial() + .passthrough(), + BraintrustModelParamsObject.passthrough(), + ]))(); + +const PromptOptionsNullish = /* @__PURE__ */ (() => + z.union([ + z + .object({ + model: z.string(), + params: ModelParams, + position: z.string(), + endpoint_name: z.union([z.string(), z.null()]), + }) + .partial(), + z.null(), + ]))(); + +const PromptParserNullish = /* @__PURE__ */ (() => + z.union([ + z.object({ + type: z.literal("llm_classifier"), + use_cot: z.boolean(), + choice_scores: z.record(z.number().gte(0).lte(1)).optional(), + choice: z.array(z.string()).optional(), + allow_no_match: z.boolean().optional(), + allow_skip: z.boolean().optional(), + }), + z.null(), + ]))(); + +const PreprocessorId = /* @__PURE__ */ (() => + z.union([ + z.object({ + type: z.literal("function"), + id: z.string(), + version: z.string().optional(), + }), + z.object({ + type: z.literal("global"), + name: z.string(), + function_type: z + .literal("preprocessor") + .optional() + .default("preprocessor"), + }), + z.object({ type: z.literal("inline"), code: z.string().min(1) }), + z.null(), + ]))(); + +const FunctionFormat = /* @__PURE__ */ (() => + z.enum(["llm", "code", "global", "graph", "topic_map"]))(); + +export const PromptData: z.ZodType< + SDK.PromptData, + z.ZodTypeDef, + SDK.SchemaInput +> = /* @__PURE__ */ (() => z .object({ - comparison_key: z.union([z.string(), z.null()]), - baseline_experiment_id: z.union([z.string(), z.null()]), - spanFieldOrder: z.union([ - z.array( - z.object({ - object_type: z.string(), - column_id: z.string(), - position: z.string(), - layout: z - .union([z.literal("full"), z.literal("two_column"), z.null()]) - .optional(), - }), - ), + prompt: PromptBlockDataNullish, + options: PromptOptionsNullish, + parser: PromptParserNullish, + preprocessor: PreprocessorId, + tool_functions: z.union([z.array(SavedFunctionId), z.null()]), + template_format: z.union([ + z.enum(["mustache", "nunjucks", "none"]), z.null(), ]), - remote_eval_sources: z.union([ - z.array( - z.object({ - url: z.string(), - name: z.union([z.string(), z.null()]).optional(), - description: z.union([z.string(), z.null()]).optional(), - }), + mcp: z.union([ + z.record( + z.union([ + z.object({ + type: z.literal("id"), + id: z.string().uuid(), + is_disabled: z.boolean().optional(), + enabled_tools: z + .union([z.array(z.string()), z.null()]) + .optional(), + }), + z.object({ + type: z.literal("url"), + url: z.string(), + is_disabled: z.boolean().optional(), + enabled_tools: z + .union([z.array(z.string()), z.null()]) + .optional(), + }), + ]), ), z.null(), ]), - disable_realtime_queries: z.union([z.boolean(), z.null()]), - monitor_charts_use_metrics_start: z.union([z.boolean(), z.null()]), - coding_agent_insights_dashboard: z.union([z.boolean(), z.null()]), - require_all_human_review_scores: z.union([z.boolean(), z.null()]), - blind_reviews: z.union([z.boolean(), z.null()]), - default_preprocessor: z.union([...SavedFunctionId.options, z.null()]), + origin: z.union([ + z + .object({ + prompt_id: z.string(), + project_id: z.string(), + prompt_version: z.string(), + }) + .partial(), + z.null(), + ]), }) - .partial(), - z.null(), -]); - -export const Project = z.object({ - id: z.string().uuid(), - org_id: z.string().uuid(), - name: z.string(), - description: z.union([z.string(), z.null()]).optional(), - created: z.union([z.string(), z.null()]).optional(), - deleted_at: z.union([z.string(), z.null()]).optional(), - user_id: z.union([z.string(), z.null()]).optional(), - settings: ProjectSettings.optional(), -}); - -export const Prompt = z.object({ - id: z.string().uuid(), - _xact_id: z.string(), - project_id: z.string().uuid(), - log_id: z.literal("p"), - org_id: z.string().uuid(), - name: z.string(), - slug: z.string(), - description: z.union([z.string(), z.null()]).optional(), - created: z.union([z.string(), z.null()]).optional(), - prompt_data: z.union([PromptData, z.null()]).optional(), - tags: z.union([z.array(z.string()), z.null()]).optional(), - metadata: z - .union([z.object({}).partial().passthrough(), z.null()]) - .optional(), - function_type: z.union([FunctionTypeEnum, z.null()]).optional(), -}); - -export const SSEConsoleEventData = z.object({ - stream: z.enum(["stderr", "stdout"]), - message: z.string(), -}); - -export const SSEProgressEventData = z.object({ - id: z.string(), - object_type: FunctionObjectType, - origin: z.union([ObjectReference, z.null()]).and(z.unknown()).optional(), - format: FunctionFormat, - output_type: FunctionOutputType, - name: z.string(), - event: z.enum([ - "reasoning_delta", - "text_delta", - "json_delta", - "error", - "console", - "start", - "done", - "progress", - ]), - data: z.string(), -}); + .partial())(); + +export const FunctionId: z.ZodType< + SDK.FunctionId, + z.ZodTypeDef, + SDK.SchemaInput +> = /* @__PURE__ */ (() => + z.union([ + z.object({ function_id: z.string(), version: z.string().optional() }), + z.object({ + project_name: z.string(), + slug: z.string(), + version: z.string().optional(), + }), + z.object({ + global_function: z.string(), + function_type: FunctionTypeEnum.optional().default("scorer"), + }), + z.object({ + prompt_session_id: z.string(), + prompt_session_function_id: z.string(), + version: z.string().optional(), + }), + z.object({ + inline_context: z.object({ + runtime: z.enum(["node", "python", "browser", "quickjs"]), + version: z.string(), + }), + code: z.string(), + function_type: FunctionTypeEnum.and(z.unknown()).optional(), + name: z.union([z.string(), z.null()]).optional(), + }), + z.object({ + inline_prompt: PromptData.optional(), + inline_function: z.object({}).partial().passthrough(), + function_type: FunctionTypeEnum.optional().default("scorer"), + name: z.union([z.string(), z.null()]).optional(), + }), + z.object({ + inline_prompt: PromptData, + function_type: FunctionTypeEnum.optional().default("scorer"), + name: z.union([z.string(), z.null()]).optional(), + }), + ]))(); + +const FunctionObjectType = /* @__PURE__ */ (() => + z.enum([ + "prompt", + "tool", + "scorer", + "task", + "workflow", + "custom_view", + "preprocessor", + "facet", + "classifier", + "parameters", + "sandbox", + ]))(); + +const FunctionOutputType = /* @__PURE__ */ (() => + z.enum(["completion", "score", "facet", "classification", "any"]))(); + +export const GitMetadataSettings: z.ZodType = + /* @__PURE__ */ (() => + z.object({ + collect: z.enum(["all", "none", "some"]), + fields: z + .array( + z.enum([ + "commit", + "branch", + "tag", + "dirty", + "author_name", + "author_email", + "commit_message", + "commit_time", + "git_diff", + ]), + ) + .optional(), + }))(); + +const ProjectSettings = /* @__PURE__ */ (() => + z.union([ + z + .object({ + comparison_key: z.union([z.string(), z.null()]), + baseline_experiment_id: z.union([z.string(), z.null()]), + spanFieldOrder: z.union([ + z.array( + z.object({ + object_type: z.string(), + column_id: z.string(), + position: z.string(), + layout: z + .union([z.literal("full"), z.literal("two_column"), z.null()]) + .optional(), + }), + ), + z.null(), + ]), + remote_eval_sources: z.union([ + z.array( + z.object({ + url: z.string(), + name: z.union([z.string(), z.null()]).optional(), + description: z.union([z.string(), z.null()]).optional(), + }), + ), + z.null(), + ]), + disable_realtime_queries: z.union([z.boolean(), z.null()]), + monitor_charts_use_metrics_start: z.union([z.boolean(), z.null()]), + coding_agent_insights_dashboard: z.union([z.boolean(), z.null()]), + require_all_human_review_scores: z.union([z.boolean(), z.null()]), + blind_reviews: z.union([z.boolean(), z.null()]), + default_preprocessor: z.union([...SavedFunctionId.options, z.null()]), + }) + .partial(), + z.null(), + ]))(); + +export const Project: z.ZodType< + SDK.ProjectRecord, + z.ZodTypeDef, + SDK.SchemaInput +> = /* @__PURE__ */ (() => + z.object({ + id: z.string().uuid(), + org_id: z.string().uuid(), + name: z.string(), + description: z.union([z.string(), z.null()]).optional(), + created: z.union([z.string(), z.null()]).optional(), + deleted_at: z.union([z.string(), z.null()]).optional(), + user_id: z.union([z.string(), z.null()]).optional(), + settings: ProjectSettings.optional(), + }))(); + +export const Prompt: z.ZodType< + SDK.PromptRecord, + z.ZodTypeDef, + SDK.SchemaInput +> = /* @__PURE__ */ (() => + z.object({ + id: z.string().uuid(), + _xact_id: z.string(), + project_id: z.string().uuid(), + log_id: z.literal("p"), + org_id: z.string().uuid(), + name: z.string(), + slug: z.string(), + description: z.union([z.string(), z.null()]).optional(), + created: z.union([z.string(), z.null()]).optional(), + prompt_data: z.union([PromptData, z.null()]).optional(), + tags: z.union([z.array(z.string()), z.null()]).optional(), + metadata: z + .union([z.object({}).partial().passthrough(), z.null()]) + .optional(), + function_type: z.union([FunctionTypeEnum, z.null()]).optional(), + }))(); + +export const SSEConsoleEventData: z.ZodType = + /* @__PURE__ */ (() => + z.object({ + stream: z.enum(["stderr", "stdout"]), + message: z.string(), + }))(); + +export const SSEProgressEventData: z.ZodType = + /* @__PURE__ */ (() => + z.object({ + id: z.string(), + object_type: FunctionObjectType, + origin: z.union([ObjectReference, z.null()]).and(z.unknown()).optional(), + format: FunctionFormat, + output_type: FunctionOutputType, + name: z.string(), + event: z.enum([ + "reasoning_delta", + "text_delta", + "json_delta", + "error", + "console", + "start", + "done", + "progress", + ]), + data: z.string(), + }))(); + +export const BraintrustAttachmentReference: z.ZodType = + BraintrustAttachmentReferenceObject; + +export const ExternalAttachmentReference: z.ZodType = + ExternalAttachmentReferenceObject; diff --git a/js/src/sdk-types.ts b/js/src/sdk-types.ts index bc67f57ed..58797088a 100644 --- a/js/src/sdk-types.ts +++ b/js/src/sdk-types.ts @@ -581,3 +581,72 @@ export interface ToolFunctionDefinition { strict?: (boolean | null) | undefined; }; } + +export interface ProjectRecord { + id: string; + org_id: string; + name: string; + description?: string | null; + created?: string | null; + deleted_at?: string | null; + user_id?: string | null; + settings?: { + comparison_key?: string | null; + baseline_experiment_id?: string | null; + spanFieldOrder?: + | { + object_type: string; + column_id: string; + position: string; + layout?: "full" | "two_column" | null; + }[] + | null; + remote_eval_sources?: + | { url: string; name?: string | null; description?: string | null }[] + | null; + disable_realtime_queries?: boolean | null; + monitor_charts_use_metrics_start?: boolean | null; + coding_agent_insights_dashboard?: boolean | null; + require_all_human_review_scores?: boolean | null; + blind_reviews?: boolean | null; + default_preprocessor?: SavedFunctionId | null; + } | null; +} + +export interface PromptRecord { + id: string; + _xact_id: string; + project_id: string; + log_id: "p"; + org_id: string; + name: string; + slug: string; + description?: string | null; + created?: string | null; + prompt_data?: PromptData | null; + tags?: string[] | null; + metadata?: Record | null; + function_type?: FunctionTypeEnum | null; +} + +// JSON inputs may omit values filled by schema defaults. Keep this type plain +// so declarations do not expose Zod's implementation graph. +export type SchemaInput = T extends { type: "text"; text: string } + ? { [K in keyof Omit]: SchemaInput[K]> } & { + text?: string; + } + : T extends { role: "tool"; tool_call_id: string } + ? { + [K in keyof Omit]: SchemaInput< + Omit[K] + >; + } & { tool_call_id?: string } + : T extends { function_type: FunctionTypeEnum } + ? { + [K in keyof Omit]: SchemaInput< + Omit[K] + >; + } & { function_type?: T["function_type"] } + : T extends object + ? { [K in keyof T]: SchemaInput } + : T; diff --git a/js/src/span-cache.test.ts b/js/src/span-cache.test.ts index 37760b00f..30017032a 100644 --- a/js/src/span-cache.test.ts +++ b/js/src/span-cache.test.ts @@ -6,8 +6,8 @@ import { afterEach, beforeAll, vi, -} from "vitest"; -import iso from "./isomorph"; +} from "vite-plus/test"; +import iso from "#braintrust-platform"; import { SpanCache } from "./span-cache"; import { configureNode } from "./node/config"; diff --git a/js/src/span-cache.ts b/js/src/span-cache.ts index c66a3d1d4..7b197d40e 100644 --- a/js/src/span-cache.ts +++ b/js/src/span-cache.ts @@ -9,7 +9,7 @@ * the cache becomes a no-op (all lookups return undefined). */ -import iso from "./isomorph"; +import iso from "#braintrust-platform"; import { mergeDicts } from "../util/object_util"; // Global registry of active span caches for process exit cleanup diff --git a/js/src/span-customizer.test.ts b/js/src/span-customizer.test.ts index 5fccf1c9f..08e4904d4 100644 --- a/js/src/span-customizer.test.ts +++ b/js/src/span-customizer.test.ts @@ -6,18 +6,18 @@ import { expectTypeOf, test, vi, -} from "vitest"; +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "./logger/testing"; import { - _exportsForTestingOnly, _internalGetGlobalState, Attachment, BraintrustState, - Dataset, initLogger, SpanImpl, updateSpan, type TestBackgroundLogger, -} from "./logger"; +} from "./logger/core"; +import { Dataset } from "./logger/datasets"; import { setSpanCustomizers } from "./span-customizer"; import { configureNode } from "./node/config"; import { diff --git a/js/src/span-origin.test.ts b/js/src/span-origin.test.ts index e78cc6c4c..16a8eb377 100644 --- a/js/src/span-origin.test.ts +++ b/js/src/span-origin.test.ts @@ -1,6 +1,6 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; -import iso from "./isomorph"; +import iso from "#braintrust-platform"; import { detectSpanOriginEnvironment, getSpanInstrumentationName, diff --git a/js/src/span-origin.ts b/js/src/span-origin.ts index 755fc43f5..939d8a117 100644 --- a/js/src/span-origin.ts +++ b/js/src/span-origin.ts @@ -1,4 +1,4 @@ -import iso from "./isomorph"; +import iso from "#braintrust-platform"; declare const __BRAINTRUST_SDK_VERSION__: string; @@ -57,7 +57,7 @@ type SpanOrigin = { environment?: SpanOriginEnvironment; }; -export const INTERNAL_SPAN_INSTRUMENTATION_NAME = Symbol.for( +export const INTERNAL_SPAN_INSTRUMENTATION_NAME: unique symbol = Symbol.for( "braintrust.spanInstrumentationName", ); diff --git a/js/src/stackutil.ts b/js/src/stackutil.ts index 13933f8bf..e8d54d6ad 100644 --- a/js/src/stackutil.ts +++ b/js/src/stackutil.ts @@ -1,4 +1,4 @@ -import iso, { CallerLocation } from "./isomorph"; +import iso, { CallerLocation } from "#braintrust-platform"; interface StackTraceEntry { functionName: string; @@ -31,8 +31,7 @@ function getStackTrace(): StackTraceEntry[] { return out; } -// Fetches the first StackTraceEntry not contained inside the same directory as -// this file and converts it into a CallerLocation. +// Skip SDK frames, including feature modules nested below this directory. export function getCallerLocation(): CallerLocation | undefined { let thisDir: string | undefined = undefined; const entries = getStackTrace(); @@ -40,7 +39,11 @@ export function getCallerLocation(): CallerLocation | undefined { if (thisDir === undefined) { thisDir = iso.pathDirname?.(frame.fileName); } - if (iso.pathDirname?.(frame.fileName) !== thisDir) { + if ( + thisDir !== undefined && + !frame.fileName.startsWith(`${thisDir}/`) && + !frame.fileName.startsWith(`${thisDir}\\`) + ) { return { caller_functionname: frame.functionName, caller_filename: frame.fileName, diff --git a/js/src/template/mustache-utils.ts b/js/src/template/mustache-utils.ts index 1cfc24551..69f9d2e70 100644 --- a/js/src/template/mustache-utils.ts +++ b/js/src/template/mustache-utils.ts @@ -4,7 +4,7 @@ import Mustache from "mustache"; export function lintTemplate( template: string, context: Record, -) { +): void { const variables = getMustacheVars(template); for (const variable of variables) { const arrPathsReplaced = variable[1].replaceAll(/\.\d+/g, ".0"); diff --git a/js/src/template/registry.ts b/js/src/template/registry.ts index 8c6faa704..7a78089e9 100644 --- a/js/src/template/registry.ts +++ b/js/src/template/registry.ts @@ -97,7 +97,15 @@ class TemplatePluginRegistry { } } -const templateRegistry = new TemplatePluginRegistry(); +let templateRegistry: TemplatePluginRegistry | undefined; + +function getTemplateRegistry(): TemplatePluginRegistry { + if (!templateRegistry) { + templateRegistry = new TemplatePluginRegistry(); + templateRegistry.register(mustachePlugin); + } + return templateRegistry; +} /** * Register a template plugin and optionally activate it @@ -106,8 +114,9 @@ const templateRegistry = new TemplatePluginRegistry(); * If `options` is omitted but the plugin defines `defaultOptions`, the * registry will activate the renderer using those defaults. */ -export const registerTemplatePlugin = - templateRegistry.register.bind(templateRegistry); +export function registerTemplatePlugin(plugin: TemplateRendererPlugin): void { + getTemplateRegistry().register(plugin); +} /** * Gets an active template renderer by name. @@ -127,6 +136,8 @@ export const registerTemplatePlugin = * } * ``` */ -export const getTemplateRenderer = templateRegistry.get.bind(templateRegistry); - -registerTemplatePlugin(mustachePlugin); +export function getTemplateRenderer( + name: string, +): TemplateRenderer | undefined { + return getTemplateRegistry().get(name); +} diff --git a/js/src/template/renderer.test.ts b/js/src/template/renderer.test.ts index 3e7a8ed64..e63e07359 100644 --- a/js/src/template/renderer.test.ts +++ b/js/src/template/renderer.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe } from "vitest"; +import { expect, test, describe } from "vite-plus/test"; import { parseTemplateFormat, isTemplateFormat, diff --git a/js/src/trace.test.ts b/js/src/trace.test.ts index 3c3876b1b..41dbb4773 100644 --- a/js/src/trace.test.ts +++ b/js/src/trace.test.ts @@ -6,7 +6,7 @@ import { beforeAll, beforeEach, afterEach, -} from "vitest"; +} from "vite-plus/test"; import { CachedSpanFetcher, LocalTrace, @@ -14,11 +14,8 @@ import { SpanFetchFn, SpanFilters, } from "./trace"; -import { - _exportsForTestingOnly, - _internalGetGlobalState, - type BraintrustState, -} from "./logger"; +import { _exportsForTestingOnly } from "./logger/testing"; +import { _internalGetGlobalState, type BraintrustState } from "./logger/core"; import { configureNode } from "./node/config"; // Mock the invoke function diff --git a/js/src/trace.ts b/js/src/trace.ts index 5f4554723..c85d8b9bc 100644 --- a/js/src/trace.ts +++ b/js/src/trace.ts @@ -1,4 +1,5 @@ -import { BraintrustState, ObjectFetcher, WithTransactionId } from "./logger"; +import { BraintrustState } from "./logger/core"; +import { ObjectFetcher, WithTransactionId } from "./logger/objects"; import { invoke } from "./functions/invoke"; interface TraceOptions { @@ -732,7 +733,11 @@ export class LocalTrace implements Trace { ); } - getConfiguration() { + getConfiguration(): { + object_type: "project_logs" | "experiment" | "playground_logs"; + object_id: string; + root_span_id: string; + } { return { object_type: this.objectType, object_id: this.objectId, @@ -744,7 +749,13 @@ export class LocalTrace implements Trace { * Custom JSON serialization - returns trace_ref format so LocalTrace * can be safely passed through JSON.stringify() (e.g., in invoke()). */ - toJSON() { + toJSON(): { + trace_ref: { + object_type: "project_logs" | "experiment" | "playground_logs"; + object_id: string; + root_span_id: string; + }; + } { return { trace_ref: { object_type: this.objectType, diff --git a/js/src/util.test.ts b/js/src/util.test.ts index a6e98b579..3965d5049 100644 --- a/js/src/util.test.ts +++ b/js/src/util.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { LazyValue } from "./util"; test("LazyValue evaluates exactly once", async () => { diff --git a/js/src/util.ts b/js/src/util.ts index 97b2b0dc9..44e289299 100644 --- a/js/src/util.ts +++ b/js/src/util.ts @@ -125,7 +125,7 @@ export class SyncLazyValue { export function addAzureBlobHeaders( headers: Record, url: string, -) { +): void { // According to https://stackoverflow.com/questions/37824136/put-on-sas-blob-url-without-specifying-x-ms-blob-type-header, // there is no way to avoid including this. if (url.includes("blob.core.windows.net")) { @@ -142,7 +142,10 @@ export class InternalAbortError extends Error { } // Return a copy of record with the given keys removed. -export function filterFrom(record: Record, keys: string[]) { +export function filterFrom( + record: Record, + keys: string[], +): Record { const out: Record = {}; for (const k of Object.keys(record)) { if (!keys.includes(k)) { diff --git a/js/src/vendor-sdk-types/langchain.ts b/js/src/vendor-sdk-types/langchain.ts index fd795b8b5..e3b0c8191 100644 --- a/js/src/vendor-sdk-types/langchain.ts +++ b/js/src/vendor-sdk-types/langchain.ts @@ -1,4 +1,4 @@ -import type { Logger, Span, StartSpanArgs } from "../logger"; +import type { Logger, Span, StartSpanArgs } from "../logger/core"; import type { ExperimentLogPartialArgs } from "../util"; export type LangChainSerialized = { diff --git a/js/src/workerd/config.ts b/js/src/workerd/config.ts index bdd813520..f4983fa8e 100644 --- a/js/src/workerd/config.ts +++ b/js/src/workerd/config.ts @@ -1,49 +1,7 @@ -import iso from "../isomorph"; -import { _internalSetInitialState } from "../logger"; -import { resolveRuntimeAsyncLocalStorage } from "../runtime-async-local-storage"; -import { registry } from "../instrumentation/registry"; +import iso from "#braintrust-platform"; +import { createWorkerdIsomorph } from "./platform"; -let workerdConfigured = false; - -/** - * Configure the isomorph for Cloudflare Workers (workerd) runtime. - */ +/** @internal Configure source-level tests and compatibility integrations. */ export function configureWorkerd(): void { - if (workerdConfigured) { - return; - } - - iso.buildType = "workerd"; - - const runtimeAsyncLocalStorage = resolveRuntimeAsyncLocalStorage(); - - if (runtimeAsyncLocalStorage) { - iso.newAsyncLocalStorage = () => new runtimeAsyncLocalStorage(); - } - - iso.getEnv = (name: string) => { - if (typeof process === "undefined" || typeof process.env === "undefined") { - return undefined; - } - return process.env[name]; - }; - iso.getBraintrustApiKey = async () => { - const value = iso.getEnv("BRAINTRUST_API_KEY"); - return value?.trim() ? value : undefined; - }; - - iso.hash = (data: string): string => { - let hash = 0; - for (let i = 0; i < data.length; i++) { - const char = data.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; - } - const hashHex = (hash >>> 0).toString(16).padStart(8, "0"); - return hashHex.repeat(8).substring(0, 64); - }; - - _internalSetInitialState(); - registry.enable(); - workerdConfigured = true; + Object.assign(iso, createWorkerdIsomorph()); } diff --git a/js/src/workerd/index.ts b/js/src/workerd/index.ts index 3fa5cdb19..233c0b831 100644 --- a/js/src/workerd/index.ts +++ b/js/src/workerd/index.ts @@ -5,8 +5,4 @@ * runtime environments. */ -import { configureWorkerd } from "./config"; - -configureWorkerd(); - export * from "../exports"; diff --git a/js/src/workerd/isomorph.ts b/js/src/workerd/isomorph.ts new file mode 100644 index 000000000..1493a918c --- /dev/null +++ b/js/src/workerd/isomorph.ts @@ -0,0 +1,10 @@ +import type { Common } from "../isomorph-base"; +export type { + CallerLocation, + IsoAsyncLocalStorage, + IsoTracingChannel, + IsoChannelHandlers, +} from "../isomorph-base"; +import { createWorkerdIsomorph } from "./platform"; +const iso: Common = /* @__PURE__ */ createWorkerdIsomorph(); +export default iso; diff --git a/js/src/workerd/platform.ts b/js/src/workerd/platform.ts new file mode 100644 index 000000000..e611d8c49 --- /dev/null +++ b/js/src/workerd/platform.ts @@ -0,0 +1,43 @@ +import { createIsomorph, type Common } from "../isomorph-base"; +import { resolveRuntimeAsyncLocalStorage } from "../runtime-async-local-storage"; + +/** + * Configure the isomorph for Cloudflare Workers (workerd) runtime. + */ +export function createWorkerdIsomorph(): Common { + const iso = createIsomorph(); + + iso.buildType = "workerd"; + + const fallbackAsyncLocalStorage = iso.newAsyncLocalStorage; + iso.newAsyncLocalStorage = () => { + const RuntimeAsyncLocalStorage = resolveRuntimeAsyncLocalStorage(); + return RuntimeAsyncLocalStorage + ? new RuntimeAsyncLocalStorage() + : fallbackAsyncLocalStorage(); + }; + + iso.getEnv = (name: string) => { + if (typeof process === "undefined" || typeof process.env === "undefined") { + return undefined; + } + return process.env[name]; + }; + iso.getBraintrustApiKey = async () => { + const value = iso.getEnv("BRAINTRUST_API_KEY"); + return value?.trim() ? value : undefined; + }; + + iso.hash = (data: string): string => { + let hash = 0; + for (let i = 0; i < data.length; i++) { + const char = data.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + const hashHex = (hash >>> 0).toString(16).padStart(8, "0"); + return hashHex.repeat(8).substring(0, 64); + }; + + return iso; +} diff --git a/js/src/workflow-eval.test.ts b/js/src/workflow-eval.test.ts index 52e4cf691..ae4bf3106 100644 --- a/js/src/workflow-eval.test.ts +++ b/js/src/workflow-eval.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, expectTypeOf, test, vi } from "vitest"; +import { describe, expect, expectTypeOf, test, vi } from "vite-plus/test"; import { configureNode } from "./node/config"; import { WorkflowBatchScorer, diff --git a/js/src/workflow-eval.ts b/js/src/workflow-eval.ts index 8e5ee9571..ddce7959e 100644 --- a/js/src/workflow-eval.ts +++ b/js/src/workflow-eval.ts @@ -29,20 +29,19 @@ import { type OneOrMoreScores, runEvaluator, } from "./framework"; -import iso from "./isomorph"; +import iso from "#braintrust-platform"; import { type BaseMetadata, type DefaultMetadataType, type EvalCase, - type Experiment, - type ExperimentSummary, NOOP_SPAN, type Span, _internalResumeSpan, _internalStartSpanWithInitialMerge, logError as logSpanError, newId, -} from "./logger"; +} from "./logger/core"; +import { type Experiment, type ExperimentSummary } from "./logger/experiments"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -94,20 +93,23 @@ export class WorkflowEvalMemoryStore implements WorkflowEvalStore { this.values.set(key, value.slice()); } - async getOrSet(key: string, value: Uint8Array) { + async getOrSet( + key: string, + value: Uint8Array, + ): Promise<{ value: Uint8Array; created: boolean }> { const existing = this.values.get(key); if (existing) return { value: existing.slice(), created: false }; this.values.set(key, value.slice()); return { value: value.slice(), created: true }; } - async addToSet(key: string, member: string) { + async addToSet(key: string, member: string): Promise { let members = this.sets.get(key); if (!members) this.sets.set(key, (members = new Set())); members.add(member); } - async getSetSize(key: string) { + async getSetSize(key: string): Promise { return this.sets.get(key)?.size ?? 0; } } @@ -181,7 +183,10 @@ export class WorkflowEvalRedisStore implements WorkflowEvalStore { } } - async getOrSet(key: string, value: Uint8Array) { + async getOrSet( + key: string, + value: Uint8Array, + ): Promise<{ value: Uint8Array; created: boolean }> { const redisKey = `${this.keyPrefix}${key}`; const encoded = uint8ArrayToBase64(value); const client = this.client as typeof this.client & { @@ -214,7 +219,7 @@ export class WorkflowEvalRedisStore implements WorkflowEvalStore { return { value: base64ToUint8Array(existing), created: false }; } - async addToSet(key: string, member: string) { + async addToSet(key: string, member: string): Promise { await this.evalSet( "redis.call('SADD', KEYS[1], ARGV[1]); redis.call('PEXPIRE', KEYS[1], ARGV[2]); return 1", key, @@ -222,7 +227,7 @@ export class WorkflowEvalRedisStore implements WorkflowEvalStore { ); } - async getSetSize(key: string) { + async getSetSize(key: string): Promise { return this.evalSet("return redis.call('SCARD', KEYS[1])", key, []); } diff --git a/js/src/wrappers/ai-sdk-evaluate.test.ts b/js/src/wrappers/ai-sdk-evaluate.test.ts index 180907938..c9cb493c0 100644 --- a/js/src/wrappers/ai-sdk-evaluate.test.ts +++ b/js/src/wrappers/ai-sdk-evaluate.test.ts @@ -1,15 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/consistent-type-assertions */ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { - _exportsForTestingOnly, - initLogger, - traced, - type TestBackgroundLogger, -} from "../logger"; + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger, traced, type TestBackgroundLogger } from "../logger/core"; import { configureNode } from "../node/config"; import { aiSDKChannels } from "../instrumentation/providers/ai-sdk-channels"; -import { registry } from "../instrumentation/registry"; +import { registerInstrumentation } from "../instrumentation/registrations/aisdk"; import { runWithAutoInstrumentationSuppressed } from "../instrumentation/auto-instrumentation-suppression"; import type { AISDKEvaluateParams, @@ -213,8 +216,8 @@ describe("AI SDK evaluate instrumentation", () => { const wrapped = wrapAISDK({ experimental_evaluate: (_params: AISDKEvaluateParams) => original, }); - registry.enable(); - registry.enable(); + registerInstrumentation(); + registerInstrumentation(); expect(wrapped.experimental_evaluate(params)).toBe(original); await original; expect(await backgroundLogger.drain()).toHaveLength(1); diff --git a/js/src/wrappers/ai-sdk/ai-sdk-generate-image.test.ts b/js/src/wrappers/ai-sdk/ai-sdk-generate-image.test.ts index ccba4814e..cb0334a67 100644 --- a/js/src/wrappers/ai-sdk/ai-sdk-generate-image.test.ts +++ b/js/src/wrappers/ai-sdk/ai-sdk-generate-image.test.ts @@ -1,11 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/consistent-type-assertions */ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { - _exportsForTestingOnly, - initLogger, - TestBackgroundLogger, -} from "../../logger"; + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger, TestBackgroundLogger } from "../../logger/core"; import { configureNode } from "../../node/config"; import { wrapAISDK } from "./ai-sdk"; diff --git a/js/src/wrappers/ai-sdk/ai-sdk.test.ts b/js/src/wrappers/ai-sdk/ai-sdk.test.ts index cc07edbdd..78c6a118b 100644 --- a/js/src/wrappers/ai-sdk/ai-sdk.test.ts +++ b/js/src/wrappers/ai-sdk/ai-sdk.test.ts @@ -10,15 +10,12 @@ import { expectTypeOf, expect, vi, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../../node/config"; import * as ai from "ai"; import { openai } from "@ai-sdk/openai"; -import { - _exportsForTestingOnly, - initLogger, - TestBackgroundLogger, -} from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { initLogger, TestBackgroundLogger } from "../../logger/core"; import { wrapAISDK, wrapAgentClass } from "./ai-sdk"; import { getCurrentUnixTimestamp } from "../../util"; import { readFileSync } from "fs"; diff --git a/js/src/wrappers/ai-sdk/ai-sdk.ts b/js/src/wrappers/ai-sdk/ai-sdk.ts index 2c108f8a5..bae79a56d 100644 --- a/js/src/wrappers/ai-sdk/ai-sdk.ts +++ b/js/src/wrappers/ai-sdk/ai-sdk.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../../instrumentation/registrations/aisdk"; /* eslint-disable @typescript-eslint/no-explicit-any */ import { SpanTypeAttribute } from "../../../util"; @@ -112,6 +113,7 @@ function isModuleNamespace(obj: unknown): obj is AISDKNamespaceObject { * ``` */ export function wrapAISDK(aiSDK: T, options: WrapAISDKOptions = {}): T { + registerInstrumentation(); // Handle null/undefined early - can't create Proxy with non-objects if (!aiSDK || typeof aiSDK !== "object") { return aiSDK; @@ -280,6 +282,7 @@ export const wrapAgentClass = ( AgentClass: any, options: WrapAISDKOptions = {}, ): any => { + registerInstrumentation(); const typedAgentClass = AgentClass as | AISDKAgentClass | AISDKWorkflowAgentClass; diff --git a/js/src/wrappers/ai-sdk/harness-agent-context.ts b/js/src/wrappers/ai-sdk/harness-agent-context.ts index 6536975e5..0d2c0ac9f 100644 --- a/js/src/wrappers/ai-sdk/harness-agent-context.ts +++ b/js/src/wrappers/ai-sdk/harness-agent-context.ts @@ -1,5 +1,8 @@ -import iso from "../../isomorph"; -import type { IsoAsyncLocalStorage, IsoTracingChannel } from "../../isomorph"; +import iso from "#braintrust-platform"; +import type { + IsoAsyncLocalStorage, + IsoTracingChannel, +} from "#braintrust-platform"; import { _internalGetGlobalState, _internalStartSpan, @@ -8,7 +11,7 @@ import { updateSpan, type Span, type StartSpanArgs, -} from "../../logger"; +} from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/wrappers/ai-sdk/normalize-logged-output.ts b/js/src/wrappers/ai-sdk/normalize-logged-output.ts index d60262fc5..291ff80df 100644 --- a/js/src/wrappers/ai-sdk/normalize-logged-output.ts +++ b/js/src/wrappers/ai-sdk/normalize-logged-output.ts @@ -1,5 +1,5 @@ import type { AISDKResult } from "../../vendor-sdk-types/ai-sdk"; -import { BaseAttachment } from "../../logger"; +import { BaseAttachment } from "../../logger/core"; const REMOVE_NORMALIZED_VALUE = Symbol("braintrust.ai-sdk.remove-normalized"); diff --git a/js/src/wrappers/ai-sdk/telemetry.ts b/js/src/wrappers/ai-sdk/telemetry.ts index ef4f328ce..f10b37cf4 100644 --- a/js/src/wrappers/ai-sdk/telemetry.ts +++ b/js/src/wrappers/ai-sdk/telemetry.ts @@ -3,7 +3,7 @@ import { getCurrentUnixTimestamp, isObject, } from "../../util"; -import { logError, startSpan, withCurrent, type Span } from "../../logger"; +import { logError, startSpan, withCurrent, type Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, diff --git a/js/src/wrappers/ai-sdk/workflow-agent-context.ts b/js/src/wrappers/ai-sdk/workflow-agent-context.ts index cb602bb62..da0e1c6e1 100644 --- a/js/src/wrappers/ai-sdk/workflow-agent-context.ts +++ b/js/src/wrappers/ai-sdk/workflow-agent-context.ts @@ -1,4 +1,4 @@ -import { _internalGetGlobalState, type Span } from "../../logger"; +import { _internalGetGlobalState, type Span } from "../../logger/core"; const workflowAgentWrapperSpans = new Map(); diff --git a/js/src/wrappers/anthropic-no-install.test.ts b/js/src/wrappers/anthropic-no-install.test.ts index b303cc542..47e64a7dd 100644 --- a/js/src/wrappers/anthropic-no-install.test.ts +++ b/js/src/wrappers/anthropic-no-install.test.ts @@ -1,4 +1,4 @@ -import { test, expect, vi } from "vitest"; +import { test, expect, vi } from "vite-plus/test"; import { collectAnthropicSession } from "./anthropic-session-collector"; import { wrapAnthropic } from "./anthropic"; diff --git a/js/src/wrappers/anthropic-session-collector.ts b/js/src/wrappers/anthropic-session-collector.ts index 849b0e8d4..b8f38f010 100644 --- a/js/src/wrappers/anthropic-session-collector.ts +++ b/js/src/wrappers/anthropic-session-collector.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/anthropic"; import { debugLogger } from "../debug-logger"; const sessionStreamCollectors = new WeakMap void>(); @@ -21,6 +22,7 @@ export function registerAnthropicSessionStreamCollector( * @returns The same stream object. */ export function collectAnthropicSession(stream: T): T { + registerInstrumentation(); const collector = sessionStreamCollectors.get(stream); if (!collector) { return stream; diff --git a/js/src/wrappers/anthropic-tokens-util.test.ts b/js/src/wrappers/anthropic-tokens-util.test.ts index b7f1fcc55..9b71f7ed2 100644 --- a/js/src/wrappers/anthropic-tokens-util.test.ts +++ b/js/src/wrappers/anthropic-tokens-util.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { finalizeAnthropicTokens, toNumericMetrics, diff --git a/js/src/wrappers/anthropic.ts b/js/src/wrappers/anthropic.ts index 9620731b6..0da5ed33c 100644 --- a/js/src/wrappers/anthropic.ts +++ b/js/src/wrappers/anthropic.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/anthropic"; import { anthropicChannels } from "../instrumentation/providers/anthropic-channels"; import { TypedApplyProxy } from "../typed-instrumentation-helpers"; import type { @@ -26,6 +27,7 @@ import type { * @returns The wrapped `Anthropic` object. */ export function wrapAnthropic(anthropic: T): T { + registerInstrumentation(); const au: unknown = anthropic; if ( au && diff --git a/js/src/wrappers/attachment-utils.test.ts b/js/src/wrappers/attachment-utils.test.ts index 1ba582e27..3cf8e1d2c 100644 --- a/js/src/wrappers/attachment-utils.test.ts +++ b/js/src/wrappers/attachment-utils.test.ts @@ -1,6 +1,6 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import iso from "../isomorph"; -import { _internalCaptureAttachmentsEnabled, Attachment } from "../logger"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; +import iso from "#braintrust-platform"; +import { _internalCaptureAttachmentsEnabled, Attachment } from "../logger/core"; import { processInputAttachments } from "./attachment-utils"; const originalGetEnv = iso.getEnv; diff --git a/js/src/wrappers/attachment-utils.ts b/js/src/wrappers/attachment-utils.ts index eb6123845..be4837627 100644 --- a/js/src/wrappers/attachment-utils.ts +++ b/js/src/wrappers/attachment-utils.ts @@ -2,7 +2,7 @@ import { Attachment, BaseAttachment, _internalCaptureAttachmentsEnabled, -} from "../logger"; +} from "../logger/core"; /** Remove media fields without reading their values, dropping type-only blocks. */ export function omitMediaData( diff --git a/js/src/wrappers/bedrock-runtime.test.ts b/js/src/wrappers/bedrock-runtime.test.ts index e2a8d606a..a99a9bb14 100644 --- a/js/src/wrappers/bedrock-runtime.test.ts +++ b/js/src/wrappers/bedrock-runtime.test.ts @@ -6,9 +6,10 @@ import { expect, test, vi, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../node/config"; -import { _exportsForTestingOnly, initLogger } from "../logger"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger } from "../logger/core"; import { wrapBedrockRuntime } from "./bedrock-runtime"; try { diff --git a/js/src/wrappers/bedrock-runtime.ts b/js/src/wrappers/bedrock-runtime.ts index 4e60fd912..658f2d9d1 100644 --- a/js/src/wrappers/bedrock-runtime.ts +++ b/js/src/wrappers/bedrock-runtime.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/awsBedrockRuntime"; import { runWithAutoInstrumentationSuppressed } from "../instrumentation/auto-instrumentation-suppression"; import { bedrockRuntimeChannels } from "../instrumentation/providers/bedrock-runtime-channels"; import { @@ -13,6 +14,7 @@ import type { * Wrap an AWS Bedrock Runtime client with Braintrust tracing. */ export function wrapBedrockRuntime(client: T): T { + registerInstrumentation(); if (isSupportedBedrockRuntimeClient(client)) { return bedrockRuntimeProxy(client) as T; } diff --git a/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts b/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts index 9ffb77334..ab7bfb348 100644 --- a/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts +++ b/js/src/wrappers/claude-agent-sdk/claude-agent-sdk.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../../instrumentation/registrations/claudeAgentSDK"; import { claudeAgentSDKChannels } from "../../instrumentation/providers/claude-agent-sdk-channels"; import { CLAUDE_AGENT_SDK_SKIP_LOCAL_TOOL_HOOKS_OPTION } from "../../instrumentation/providers/claude-agent-sdk-instrumentation-constants"; import { wrapLocalClaudeToolHandler } from "../../instrumentation/providers/claude-agent-sdk-local-tool-spans"; @@ -20,6 +21,7 @@ type LocalToolMetadata = { * @returns Object with wrapped query, tool, and createSdkMcpServer functions */ export function wrapClaudeAgentSDK(sdk: T): T { + registerInstrumentation(); const s: unknown = sdk; if ( s && diff --git a/js/src/wrappers/cloudflare-agent.test.ts b/js/src/wrappers/cloudflare-agent.test.ts index 54805fe8f..2762fb197 100644 --- a/js/src/wrappers/cloudflare-agent.test.ts +++ b/js/src/wrappers/cloudflare-agent.test.ts @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const { tracePromise } = vi.hoisted(() => ({ tracePromise: vi.fn((fn: () => Promise, _event?: unknown) => fn()), })); -vi.mock("../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { getEnv: vi.fn(), newTracingChannel: vi.fn(() => ({ diff --git a/js/src/wrappers/cloudflare-agent.ts b/js/src/wrappers/cloudflare-agent.ts index baee1d63d..a981f8364 100644 --- a/js/src/wrappers/cloudflare-agent.ts +++ b/js/src/wrappers/cloudflare-agent.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/cloudflareAgents"; import { debugLogger } from "../debug-logger"; import { cloudflareAgentsChannels } from "../instrumentation/providers/cloudflare-agents-channels"; import type { CloudflareAgent } from "../vendor-sdk-types/cloudflare-agents"; @@ -13,6 +14,7 @@ const WRAPPED_RUN_AGENT_TOOL = Symbol.for( * Object exports and inheritance continue to work unchanged. */ export function wrapCloudflareAgent(Agent: T): T { + registerInstrumentation(); if (typeof Agent !== "function") { warnUnsupportedAgent(); return Agent; diff --git a/js/src/wrappers/cloudflare-ai-chat.test.ts b/js/src/wrappers/cloudflare-ai-chat.test.ts index 123abe70a..e60a8b404 100644 --- a/js/src/wrappers/cloudflare-ai-chat.test.ts +++ b/js/src/wrappers/cloudflare-ai-chat.test.ts @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const { tracePromise } = vi.hoisted(() => ({ tracePromise: vi.fn((fn: () => Promise, _event?: unknown) => fn()), })); -vi.mock("../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { getEnv: vi.fn(() => undefined), newTracingChannel: vi.fn(() => ({ diff --git a/js/src/wrappers/cloudflare-ai-chat.ts b/js/src/wrappers/cloudflare-ai-chat.ts index 11f834feb..58423acbc 100644 --- a/js/src/wrappers/cloudflare-ai-chat.ts +++ b/js/src/wrappers/cloudflare-ai-chat.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/cloudflareAIChat"; import { debugLogger } from "../debug-logger"; import { instrumentCloudflareAIChatAgent } from "../instrumentation/providers/cloudflare-ai-chat-instrumentation"; import type { @@ -16,6 +17,7 @@ const wrappedClasses = new WeakMap(); * diagnostic-channel events as automatic instrumentation. */ export function wrapCloudflareAIChat(module: T): T { + registerInstrumentation(); if (!module || typeof module !== "object") { return module; } diff --git a/js/src/wrappers/cloudflare-think.test.ts b/js/src/wrappers/cloudflare-think.test.ts index 1db1b0f5d..92e1d836d 100644 --- a/js/src/wrappers/cloudflare-think.test.ts +++ b/js/src/wrappers/cloudflare-think.test.ts @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const { tracePromise } = vi.hoisted(() => ({ tracePromise: vi.fn((fn: () => unknown, _event?: unknown) => fn()), })); -vi.mock("../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(() => ({ subscribe: vi.fn(), diff --git a/js/src/wrappers/cloudflare-think.ts b/js/src/wrappers/cloudflare-think.ts index 2b1e371bd..55a9b5bfb 100644 --- a/js/src/wrappers/cloudflare-think.ts +++ b/js/src/wrappers/cloudflare-think.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/cloudflareThink"; import { cloudflareThinkChannels } from "../instrumentation/providers/cloudflare-think-channels"; import type { CloudflareThinkConstructor, @@ -21,6 +22,7 @@ const WRAPPED_THINK = Symbol.for("braintrust.cloudflare-think.wrapped"); * ``` */ export function wrapCloudflareThink(sdk: T): T { + registerInstrumentation(); if (!sdk || typeof sdk !== "object") { return sdk; } diff --git a/js/src/wrappers/cohere.test.ts b/js/src/wrappers/cohere.test.ts index 5c585b948..b7a2ae5b9 100644 --- a/js/src/wrappers/cohere.test.ts +++ b/js/src/wrappers/cohere.test.ts @@ -6,9 +6,10 @@ import { expect, test, vi, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../node/config"; -import { _exportsForTestingOnly, initLogger } from "../logger"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger } from "../logger/core"; import { wrapCohere } from "./cohere"; try { diff --git a/js/src/wrappers/cohere.ts b/js/src/wrappers/cohere.ts index 6c55f65dd..9108b902c 100644 --- a/js/src/wrappers/cohere.ts +++ b/js/src/wrappers/cohere.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/cohere"; import { cohereChannels } from "../instrumentation/providers/cohere-channels"; import type { CohereChatRequest, @@ -15,6 +16,7 @@ import type { * Braintrust plugins can consume. */ export function wrapCohere(cohere: T): T { + registerInstrumentation(); if (isSupportedCohereClient(cohere)) { return cohereProxy(cohere) as T; } diff --git a/js/src/wrappers/cursor-sdk.test.ts b/js/src/wrappers/cursor-sdk.test.ts index c238fba96..8f1402107 100644 --- a/js/src/wrappers/cursor-sdk.test.ts +++ b/js/src/wrappers/cursor-sdk.test.ts @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const { tracePromise } = vi.hoisted(() => ({ tracePromise: vi.fn((fn: () => Promise) => fn()), })); -vi.mock("../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(() => ({ subscribe: vi.fn(), diff --git a/js/src/wrappers/cursor-sdk.ts b/js/src/wrappers/cursor-sdk.ts index c20897b82..ed67e7d14 100644 --- a/js/src/wrappers/cursor-sdk.ts +++ b/js/src/wrappers/cursor-sdk.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/cursorSDK"; import { cursorSDKChannels } from "../instrumentation/providers/cursor-sdk-channels"; import type { CursorSDKAgent, @@ -15,6 +16,7 @@ const WRAPPED_AGENT = Symbol.for("braintrust.cursor-sdk.wrapped-agent"); * diagnostics-channel events; the Cursor SDK plugin owns span lifecycle. */ export function wrapCursorSDK(sdk: T): T { + registerInstrumentation(); if (!sdk || typeof sdk !== "object") { return sdk; } diff --git a/js/src/wrappers/elevenlabs.ts b/js/src/wrappers/elevenlabs.ts index 7f0c3116a..4c6132e59 100644 --- a/js/src/wrappers/elevenlabs.ts +++ b/js/src/wrappers/elevenlabs.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/elevenlabs"; import { isObject } from "../../util"; import { debugLogger } from "../debug-logger"; import { elevenLabsChannels } from "../instrumentation/providers/elevenlabs-channels"; @@ -25,6 +26,7 @@ const clients = new WeakMap(); * */ export function wrapElevenLabs(client: T): T { + registerInstrumentation(); if ( !isObject(client) || !isObject(client.textToSpeech) || diff --git a/js/src/wrappers/genkit.test.ts b/js/src/wrappers/genkit.test.ts index 3677dc99d..904ee8314 100644 --- a/js/src/wrappers/genkit.test.ts +++ b/js/src/wrappers/genkit.test.ts @@ -1,5 +1,5 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { IsoChannelHandlers } from "../isomorph"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; +import type { IsoChannelHandlers } from "#braintrust-platform"; import type { ChannelMessage } from "../instrumentation/core/channel-definitions"; import { genkitChannels } from "../instrumentation/providers/genkit-channels"; import { configureNode } from "../node/config"; diff --git a/js/src/wrappers/genkit.ts b/js/src/wrappers/genkit.ts index a4900ae9f..923888417 100644 --- a/js/src/wrappers/genkit.ts +++ b/js/src/wrappers/genkit.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/genkit"; import { genkitChannels } from "../instrumentation/providers/genkit-channels"; import type { GenkitAction, @@ -28,6 +29,7 @@ const wrappedGenkitActions = new WeakMap(); * - wrapping the module namespace and then calling its `genkit` factory */ export function wrapGenkit(genkit: T): T { + registerInstrumentation(); if (isGenkitInstance(genkit)) { return wrapGenkitInstance(genkit) as T; } diff --git a/js/src/wrappers/github-copilot.ts b/js/src/wrappers/github-copilot.ts index 20e76dbda..ce4ede990 100644 --- a/js/src/wrappers/github-copilot.ts +++ b/js/src/wrappers/github-copilot.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/gitHubCopilot"; import { gitHubCopilotChannels } from "../instrumentation/providers/github-copilot-channels"; import type { GitHubCopilotClient, @@ -36,6 +37,7 @@ function isGitHubCopilotClient(value: unknown): value is GitHubCopilotClient { * ``` */ export function wrapCopilotClient(client: T): T { + registerInstrumentation(); if (!isGitHubCopilotClient(client)) { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.warn( diff --git a/js/src/wrappers/google-adk.test.ts b/js/src/wrappers/google-adk.test.ts index cda99043f..b8f6f83e9 100644 --- a/js/src/wrappers/google-adk.test.ts +++ b/js/src/wrappers/google-adk.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, afterEach } from "vitest"; +import { describe, it, expect, vi, afterEach } from "vite-plus/test"; // Mock iso's newTracingChannel -vi.mock("../isomorph", () => { +vi.mock("#braintrust-platform", () => { const mockTraceSync = vi.fn((fn: () => any) => fn()); const mockTracePromise = vi.fn((fn: () => any) => fn()); return { diff --git a/js/src/wrappers/google-adk.ts b/js/src/wrappers/google-adk.ts index 6e908222e..d51b3e3de 100644 --- a/js/src/wrappers/google-adk.ts +++ b/js/src/wrappers/google-adk.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/googleADK"; import { googleADKChannels } from "../instrumentation/providers/google-adk-channels"; import type { GoogleADKRunner, @@ -40,6 +41,7 @@ import type { * ``` */ export function wrapGoogleADK>(adkModule: T): T { + registerInstrumentation(); if (!adkModule || typeof adkModule !== "object") { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.warn("Invalid Google ADK module. Not wrapping."); diff --git a/js/src/wrappers/google-genai.test.ts b/js/src/wrappers/google-genai.test.ts index 0eae7ee46..e02c8494d 100644 --- a/js/src/wrappers/google-genai.test.ts +++ b/js/src/wrappers/google-genai.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const { invoke, tracePromise } = vi.hoisted(() => ({ invoke: vi.fn( @@ -11,7 +11,7 @@ const { invoke, tracePromise } = vi.hoisted(() => ({ tracePromise: vi.fn((fn: () => Promise) => fn()), })); -vi.mock("../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(() => ({ subscribe: vi.fn(), diff --git a/js/src/wrappers/google-genai.ts b/js/src/wrappers/google-genai.ts index 8e8dac18b..7c88885a4 100644 --- a/js/src/wrappers/google-genai.ts +++ b/js/src/wrappers/google-genai.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/googleGenAI"; import { googleGenAIChannels } from "../instrumentation/providers/google-genai-channels"; import { isObject } from "../util"; import type { @@ -36,6 +37,7 @@ const patchedHttpResponses = new WeakSet(); export function wrapGoogleGenAI>( googleGenAI: T, ): T { + registerInstrumentation(); if (!googleGenAI || typeof googleGenAI !== "object") { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.warn("Invalid Google GenAI module. Not wrapping."); diff --git a/js/src/wrappers/google-generative-ai.ts b/js/src/wrappers/google-generative-ai.ts index 9d0e5c4b9..66d946731 100644 --- a/js/src/wrappers/google-generative-ai.ts +++ b/js/src/wrappers/google-generative-ai.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/googleGenerativeAI"; import { googleGenerativeAIChannels } from "../instrumentation/providers/google-generative-ai-channels"; import { isObject } from "../util"; import type { @@ -8,6 +9,7 @@ import type { /** Trace @google/generative-ai model and chat calls through Braintrust. */ export function wrapGoogleGenerativeAI(client: T): T { + registerInstrumentation(); if (!isObject(client) || typeof client.getGenerativeModel !== "function") return client; return wrapClient(client as unknown as GenerativeAIClient) as T; diff --git a/js/src/wrappers/groq.test.ts b/js/src/wrappers/groq.test.ts index 5a3c19880..d777ae052 100644 --- a/js/src/wrappers/groq.test.ts +++ b/js/src/wrappers/groq.test.ts @@ -6,10 +6,11 @@ import { expect, test, vi, -} from "vitest"; +} from "vite-plus/test"; import { Readable } from "node:stream"; import { configureNode } from "../node/config"; -import { Attachment, _exportsForTestingOnly, initLogger } from "../logger"; +import { Attachment, initLogger } from "../logger/core"; +import { _exportsForTestingOnly } from "../logger/testing"; import { wrapGroq } from "./groq"; try { diff --git a/js/src/wrappers/groq.ts b/js/src/wrappers/groq.ts index 64a3ddd31..3f0ce1d4a 100644 --- a/js/src/wrappers/groq.ts +++ b/js/src/wrappers/groq.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/groq"; import { groqChannels } from "../instrumentation/providers/groq-channels"; import type { GroqAudio, @@ -18,6 +19,7 @@ import type { * Wrap a Groq client (created with `new Groq(...)`) with Braintrust tracing. */ export function wrapGroq(groq: T): T { + registerInstrumentation(); if (isSupportedGroqClient(groq)) { return groqProxy(groq) as T; } diff --git a/js/src/wrappers/huggingface-transformers.test.ts b/js/src/wrappers/huggingface-transformers.test.ts index 31e2a1180..cafa90852 100644 --- a/js/src/wrappers/huggingface-transformers.test.ts +++ b/js/src/wrappers/huggingface-transformers.test.ts @@ -5,12 +5,9 @@ import { describe, expect, test, -} from "vitest"; -import { - _exportsForTestingOnly, - initLogger, - type TestBackgroundLogger, -} from "../logger"; +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger, type TestBackgroundLogger } from "../logger/core"; import { configureNode } from "../node/config"; import { wrapHuggingFaceTransformers } from "./huggingface-transformers"; import type { diff --git a/js/src/wrappers/huggingface-transformers.ts b/js/src/wrappers/huggingface-transformers.ts index 52d8e2d56..81da5188a 100644 --- a/js/src/wrappers/huggingface-transformers.ts +++ b/js/src/wrappers/huggingface-transformers.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/huggingface"; import { huggingFaceTransformersChannels, isSupportedHuggingFaceTransformersTask, @@ -25,6 +26,7 @@ export function wrapHuggingFaceTransformers( ): HuggingFaceTransformersPipeline; export function wrapHuggingFaceTransformers(transformers: T): T; export function wrapHuggingFaceTransformers(transformers: unknown): unknown { + registerInstrumentation(); const pipelineTask = (transformers as { task?: unknown } | null)?.task; if ( typeof transformers === "function" && diff --git a/js/src/wrappers/huggingface.test.ts b/js/src/wrappers/huggingface.test.ts index 7adc5a99b..6c14adf60 100644 --- a/js/src/wrappers/huggingface.test.ts +++ b/js/src/wrappers/huggingface.test.ts @@ -5,13 +5,10 @@ import { describe, expect, test, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../node/config"; -import { - _exportsForTestingOnly, - initLogger, - TestBackgroundLogger, -} from "../logger"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger, TestBackgroundLogger } from "../logger/core"; import { wrapHuggingFace } from "./huggingface"; try { diff --git a/js/src/wrappers/huggingface.ts b/js/src/wrappers/huggingface.ts index 13a58f515..def34abc7 100644 --- a/js/src/wrappers/huggingface.ts +++ b/js/src/wrappers/huggingface.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/huggingface"; import { huggingFaceChannels } from "../instrumentation/providers/huggingface-channels"; import { isObject } from "../../util"; import type { @@ -43,6 +44,7 @@ export function wrapHuggingFace( ): HuggingFaceClient; export function wrapHuggingFace(huggingFace: T): T; export function wrapHuggingFace(huggingFace: unknown): unknown { + registerInstrumentation(); if (isSupportedHuggingFaceModule(huggingFace)) { return moduleProxy(huggingFace); } diff --git a/js/src/wrappers/langchain/callback-handler.test.ts b/js/src/wrappers/langchain/callback-handler.test.ts index 53f61cbc2..f588ddc57 100644 --- a/js/src/wrappers/langchain/callback-handler.test.ts +++ b/js/src/wrappers/langchain/callback-handler.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vite-plus/test"; import { BraintrustLangChainCallbackHandler } from "./callback-handler"; function createHarness() { diff --git a/js/src/wrappers/langchain/callback-handler.ts b/js/src/wrappers/langchain/callback-handler.ts index 5e748c458..31dfb7695 100644 --- a/js/src/wrappers/langchain/callback-handler.ts +++ b/js/src/wrappers/langchain/callback-handler.ts @@ -1,5 +1,10 @@ -import { currentSpan, initLogger, NOOP_SPAN, startSpan } from "../../logger"; -import type { Span } from "../../logger"; +import { + currentSpan, + initLogger, + NOOP_SPAN, + startSpan, +} from "../../logger/core"; +import type { Span } from "../../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -16,7 +21,7 @@ export const BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME = "BraintrustCallbackHandler"; export class BraintrustLangChainCallbackHandler { - name = BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME; + name: string = BRAINTRUST_LANGCHAIN_CALLBACK_HANDLER_NAME; private spans = new Map(); private skippedRuns = new Set(); private parent?: Span | (() => Span); diff --git a/js/src/wrappers/langgraph-sdk.ts b/js/src/wrappers/langgraph-sdk.ts index 1bf657de2..ba0a0d697 100644 --- a/js/src/wrappers/langgraph-sdk.ts +++ b/js/src/wrappers/langgraph-sdk.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/langgraphSDK"; import { isObject } from "../../util"; import { debugLogger } from "../debug-logger"; import { langGraphSDKChannels } from "../instrumentation/providers/langgraph-sdk-channels"; @@ -7,6 +8,7 @@ const clients = new WeakMap(); /** Trace LangGraph Platform runs that the caller waits for or streams. */ export function wrapLangGraphSDK(client: T): T { + registerInstrumentation(); const runsClient = isObject(client) ? client.runs : undefined; if ( !isObject(client) || diff --git a/js/src/wrappers/langsmith.test.ts b/js/src/wrappers/langsmith.test.ts index 5c2588696..b10d4661d 100644 --- a/js/src/wrappers/langsmith.test.ts +++ b/js/src/wrappers/langsmith.test.ts @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const { tracePromise } = vi.hoisted(() => ({ tracePromise: vi.fn((fn: () => Promise, _event?: unknown) => fn()), })); -vi.mock("../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(() => ({ subscribe: vi.fn(), diff --git a/js/src/wrappers/langsmith.ts b/js/src/wrappers/langsmith.ts index 983f42dd8..1571841df 100644 --- a/js/src/wrappers/langsmith.ts +++ b/js/src/wrappers/langsmith.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/langsmith"; import { debugLogger } from "../debug-logger"; import { langSmithChannels } from "../instrumentation/providers/langsmith-channels"; import type { @@ -33,6 +34,7 @@ const WRAPPED_TRACEABLE_NAMESPACE = Symbol.for( ); export function wrapLangSmithTraceable(namespace: T): T { + registerInstrumentation(); return wrapNamespaceExport( namespace, "traceable", @@ -42,6 +44,7 @@ export function wrapLangSmithTraceable(namespace: T): T { } export function wrapLangSmithRunTrees(namespace: T): T { + registerInstrumentation(); return wrapNamespaceExport( namespace, "RunTree", @@ -51,6 +54,7 @@ export function wrapLangSmithRunTrees(namespace: T): T { } export function wrapLangSmithClient(namespace: T): T { + registerInstrumentation(); return wrapNamespaceExport( namespace, "Client", diff --git a/js/src/wrappers/mastra.test.ts b/js/src/wrappers/mastra.test.ts index 5671bf261..f62eddf8a 100644 --- a/js/src/wrappers/mastra.test.ts +++ b/js/src/wrappers/mastra.test.ts @@ -5,9 +5,10 @@ import { describe, expect, test, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../node/config"; -import { _exportsForTestingOnly, initLogger } from "../logger"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger } from "../logger/core"; import { BraintrustObservabilityExporter } from "./mastra"; try { diff --git a/js/src/wrappers/mastra.ts b/js/src/wrappers/mastra.ts index 4ecaaacbe..b77283d2d 100644 --- a/js/src/wrappers/mastra.ts +++ b/js/src/wrappers/mastra.ts @@ -28,7 +28,7 @@ import { startSpan, type StartSpanArgs, type Span, -} from "../logger"; +} from "../logger/core"; import { INSTRUMENTATION_NAMES, withSpanInstrumentationName, @@ -240,7 +240,7 @@ type SpanRecord = { * each instance maintains its own span map keyed by Mastra `spanId`. */ export class BraintrustObservabilityExporter implements MastraObservabilityExporter { - public readonly name = MASTRA_BRAINTRUST_EXPORTER_NAME; + public readonly name: "braintrust" = MASTRA_BRAINTRUST_EXPORTER_NAME; private readonly spans = new Map(); // Captured at the first SPAN_STARTED event. Mastra's observability bus may diff --git a/js/src/wrappers/mistral.ts b/js/src/wrappers/mistral.ts index 4771dda38..19dab7241 100644 --- a/js/src/wrappers/mistral.ts +++ b/js/src/wrappers/mistral.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/mistral"; import { mistralChannels } from "../instrumentation/providers/mistral-channels"; import type { MistralAgents, @@ -27,6 +28,7 @@ import type { * Wrap a Mistral client (created with `new Mistral(...)`) with Braintrust tracing. */ export function wrapMistral(mistral: T): T { + registerInstrumentation(); if (isSupportedMistralClient(mistral)) { return mistralProxy(mistral) as T; } diff --git a/js/src/wrappers/node-test/node-test-example.test.ts b/js/src/wrappers/node-test/node-test-example.test.ts index f9b45e5dd..e28d8150f 100644 --- a/js/src/wrappers/node-test/node-test-example.test.ts +++ b/js/src/wrappers/node-test/node-test-example.test.ts @@ -1,5 +1,5 @@ -import { test, describe, afterAll, beforeAll } from "vitest"; -import { currentSpan } from "../../logger"; +import { test, describe, afterAll, beforeAll } from "vite-plus/test"; +import { currentSpan } from "../../logger/core"; import { initNodeTestSuite } from "./suite"; import { setupNodeTestEnv, diff --git a/js/src/wrappers/node-test/node-test-span.test.ts b/js/src/wrappers/node-test/node-test-span.test.ts index 6bcf8da5c..5f2f2c983 100644 --- a/js/src/wrappers/node-test/node-test-span.test.ts +++ b/js/src/wrappers/node-test/node-test-span.test.ts @@ -1,5 +1,5 @@ -import { test, expect, describe, afterAll, beforeAll } from "vitest"; -import { currentSpan } from "../../logger"; +import { test, expect, describe, afterAll, beforeAll } from "vite-plus/test"; +import { currentSpan } from "../../logger/core"; import { initNodeTestSuite } from "./suite"; import { setupNodeTestEnv, diff --git a/js/src/wrappers/node-test/node-test.test.ts b/js/src/wrappers/node-test/node-test.test.ts index dea01d74d..dad63afbe 100644 --- a/js/src/wrappers/node-test/node-test.test.ts +++ b/js/src/wrappers/node-test/node-test.test.ts @@ -1,4 +1,11 @@ -import { test, expect, describe, afterAll, vi, beforeAll } from "vitest"; +import { + test, + expect, + describe, + afterAll, + vi, + beforeAll, +} from "vite-plus/test"; import { initNodeTestSuite } from "./suite"; import { setupNodeTestEnv, diff --git a/js/src/wrappers/node-test/suite.ts b/js/src/wrappers/node-test/suite.ts index 80cf58dae..c68d2b0ac 100644 --- a/js/src/wrappers/node-test/suite.ts +++ b/js/src/wrappers/node-test/suite.ts @@ -1,4 +1,4 @@ -import { init, type Experiment } from "../../logger"; +import { init, type Experiment } from "../../logger/experiments"; import { runTracedEval } from "../shared/traced-eval"; import { summarizeAndFlush } from "../shared/flush"; import type { diff --git a/js/src/wrappers/node-test/test-helpers.ts b/js/src/wrappers/node-test/test-helpers.ts index a455eae66..2b70194cf 100644 --- a/js/src/wrappers/node-test/test-helpers.ts +++ b/js/src/wrappers/node-test/test-helpers.ts @@ -1,9 +1,7 @@ -import { vi } from "vitest"; +import { vi } from "vite-plus/test"; import { configureNode } from "../../node/config"; -import { - _exportsForTestingOnly, - type TestBackgroundLogger, -} from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { type TestBackgroundLogger } from "../../logger/core"; import * as logger from "../../logger"; import type { MinimalTestContext } from "./types"; diff --git a/js/src/wrappers/oai.ts b/js/src/wrappers/oai.ts index 12ef7e7cc..76015df7c 100644 --- a/js/src/wrappers/oai.ts +++ b/js/src/wrappers/oai.ts @@ -1,10 +1,11 @@ +import { registerInstrumentation } from "../instrumentation/registrations/openai"; import type { OpenAIMediaClient, OpenAIMediaMethod, OpenAIMediaParams, } from "../vendor-sdk-types/openai-media"; /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { CompiledPrompt } from "../logger"; +import type { CompiledPrompt } from "../logger/prompts"; import { responsesProxy } from "./oai_responses"; import type { ArgsOf, @@ -47,6 +48,7 @@ declare global { * @returns The wrapped `OpenAI` object. */ export function wrapOpenAI(openai: T): T { + registerInstrumentation(); const oai: unknown = openai; if ( oai && @@ -69,7 +71,6 @@ export function wrapOpenAI(openai: T): T { return openai; } } -globalThis.__inherited_braintrust_wrap_openai = wrapOpenAI; type OpenAILike = OpenAIV4Client; diff --git a/js/src/wrappers/oai_responses.ts b/js/src/wrappers/oai_responses.ts index ce9cb7955..ac9b1a07e 100644 --- a/js/src/wrappers/oai_responses.ts +++ b/js/src/wrappers/oai_responses.ts @@ -19,7 +19,7 @@ type SpanInfo = { span_info?: ChannelSpanInfo; }; -export function responsesProxy(openai: any) { +export function responsesProxy(openai: any): any { // This was added in v4.87.0 of the openai-node library if (!openai.responses) { return openai; diff --git a/js/src/wrappers/ollama.test.ts b/js/src/wrappers/ollama.test.ts index b07dac5f4..08be9132b 100644 --- a/js/src/wrappers/ollama.test.ts +++ b/js/src/wrappers/ollama.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vite-plus/test"; import { debugLogger } from "../debug-logger"; import { ollamaChannels } from "../instrumentation/providers/ollama-channels"; import type { OllamaClient } from "../vendor-sdk-types/ollama"; diff --git a/js/src/wrappers/ollama.ts b/js/src/wrappers/ollama.ts index bcc48a937..a43b262fe 100644 --- a/js/src/wrappers/ollama.ts +++ b/js/src/wrappers/ollama.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/ollama"; import { debugLogger } from "../debug-logger"; import { ollamaChannels } from "../instrumentation/providers/ollama-channels"; import { isObject } from "../../util"; @@ -16,6 +17,7 @@ import type { * diagnostics-channel events. */ export function wrapOllama(ollama: T): T { + registerInstrumentation(); if (isSupportedOllamaClient(ollama)) { return ollamaProxy(ollama) as T; } diff --git a/js/src/wrappers/openai-codex.ts b/js/src/wrappers/openai-codex.ts index c4225e9c0..6a7cd3de5 100644 --- a/js/src/wrappers/openai-codex.ts +++ b/js/src/wrappers/openai-codex.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/openaiCodexSDK"; import { openAICodexChannels } from "../instrumentation/providers/openai-codex-channels"; import type { OpenAICodexClass, @@ -18,6 +19,7 @@ const WRAPPED_THREAD = Symbol.for("braintrust.openai-codex.wrapped-thread"); * emits diagnostics-channel events; the OpenAI Codex plugin owns span lifecycle. */ export function wrapOpenAICodexSDK(sdk: T): T { + registerInstrumentation(); if (!sdk || typeof sdk !== "object") { return sdk; } diff --git a/js/src/wrappers/openrouter-agent.test.ts b/js/src/wrappers/openrouter-agent.test.ts index c1c94e12f..27c2e63ba 100644 --- a/js/src/wrappers/openrouter-agent.test.ts +++ b/js/src/wrappers/openrouter-agent.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { wrapOpenRouterAgent } from "./openrouter-agent"; import { openRouterAgentChannels } from "../instrumentation/providers/openrouter-agent-channels"; diff --git a/js/src/wrappers/openrouter-agent.ts b/js/src/wrappers/openrouter-agent.ts index 336b2d84f..b9a820caa 100644 --- a/js/src/wrappers/openrouter-agent.ts +++ b/js/src/wrappers/openrouter-agent.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/openrouterAgent"; import { openRouterAgentChannels } from "../instrumentation/providers/openrouter-agent-channels"; import type { OpenRouterAgentClient, @@ -9,6 +10,7 @@ import type { * diagnostics-channel events consumed by the OpenRouter Agent plugin. */ export function wrapOpenRouterAgent(agent: T): T { + registerInstrumentation(); const candidate: unknown = agent; if ( candidate && diff --git a/js/src/wrappers/openrouter.test.ts b/js/src/wrappers/openrouter.test.ts index 4ee88ea24..6b1f35242 100644 --- a/js/src/wrappers/openrouter.test.ts +++ b/js/src/wrappers/openrouter.test.ts @@ -6,9 +6,10 @@ import { expect, test, vi, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../node/config"; -import { _exportsForTestingOnly, initLogger } from "../logger"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger } from "../logger/core"; import { wrapOpenRouter } from "./openrouter"; const TEST_MODEL = "openai/gpt-4.1-mini"; diff --git a/js/src/wrappers/openrouter.ts b/js/src/wrappers/openrouter.ts index ab7231b0c..87ab634c0 100644 --- a/js/src/wrappers/openrouter.ts +++ b/js/src/wrappers/openrouter.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/openrouter"; import { openRouterChannels } from "../instrumentation/providers/openrouter-channels"; import type { OpenRouterBeta, @@ -22,6 +23,7 @@ import type { * diagnostics-channel events that Braintrust plugins can consume. */ export function wrapOpenRouter(openrouter: T): T { + registerInstrumentation(); const or: unknown = openrouter; if ( or && diff --git a/js/src/wrappers/pi-coding-agent.test.ts b/js/src/wrappers/pi-coding-agent.test.ts index 04e469653..03f5d8714 100644 --- a/js/src/wrappers/pi-coding-agent.test.ts +++ b/js/src/wrappers/pi-coding-agent.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const { invoke } = vi.hoisted(() => ({ invoke: vi.fn( @@ -7,7 +7,7 @@ const { invoke } = vi.hoisted(() => ({ ), })); -vi.mock("../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(() => ({ hasInterceptors: true, diff --git a/js/src/wrappers/pi-coding-agent.ts b/js/src/wrappers/pi-coding-agent.ts index f01f6a75a..0f8ca29ad 100644 --- a/js/src/wrappers/pi-coding-agent.ts +++ b/js/src/wrappers/pi-coding-agent.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/piCodingAgent"; import { piCodingAgentChannels } from "../instrumentation/providers/pi-coding-agent-channels"; import type { PiAgentSession, @@ -13,6 +14,7 @@ const WRAPPED_PROMPT = Symbol.for("braintrust.pi-coding-agent.wrapped-prompt"); * instrumentation hook calls; the Pi Coding Agent plugin owns span lifecycle. */ export function wrapPiCodingAgentSDK(sdk: T): T { + registerInstrumentation(); if (!sdk || typeof sdk !== "object") { return sdk; } diff --git a/js/src/wrappers/shared/flush.ts b/js/src/wrappers/shared/flush.ts index ab5e89028..bb1228714 100644 --- a/js/src/wrappers/shared/flush.ts +++ b/js/src/wrappers/shared/flush.ts @@ -1,4 +1,4 @@ -import type { Experiment } from "../../logger"; +import type { Experiment } from "../../logger/experiments"; import { formatExperimentSummary } from "./format"; /** diff --git a/js/src/wrappers/shared/format.ts b/js/src/wrappers/shared/format.ts index 69687bae0..a99c07864 100644 --- a/js/src/wrappers/shared/format.ts +++ b/js/src/wrappers/shared/format.ts @@ -1,4 +1,4 @@ -import type { ExperimentSummary } from "../../logger"; +import type { ExperimentSummary } from "../../logger/experiments"; export function formatExperimentSummary(summary: ExperimentSummary): string { const lines: string[] = []; diff --git a/js/src/wrappers/shared/logging.ts b/js/src/wrappers/shared/logging.ts index 9d9003fa1..157f83d6f 100644 --- a/js/src/wrappers/shared/logging.ts +++ b/js/src/wrappers/shared/logging.ts @@ -1,4 +1,4 @@ -import { currentSpan } from "../../logger"; +import { currentSpan, type Span } from "../../logger/core"; export function logOutputs(outputs: Record): void { currentSpan().log({ output: outputs }); @@ -15,6 +15,6 @@ export function logFeedback(feedback: { }); } -export function getCurrentSpan() { +export function getCurrentSpan(): Span { return currentSpan(); } diff --git a/js/src/wrappers/shared/scorers.test.ts b/js/src/wrappers/shared/scorers.test.ts index d38cfd405..5d8023b9d 100644 --- a/js/src/wrappers/shared/scorers.test.ts +++ b/js/src/wrappers/shared/scorers.test.ts @@ -1,5 +1,5 @@ -import { afterEach, expect, test, vi } from "vitest"; -import { NOOP_SPAN } from "../../logger"; +import { afterEach, expect, test, vi } from "vite-plus/test"; +import { NOOP_SPAN } from "../../logger/core"; import { runScorers } from "./scorers"; import type { ScorerFunction } from "./types"; diff --git a/js/src/wrappers/shared/scorers.ts b/js/src/wrappers/shared/scorers.ts index b3c11ee45..500d0a306 100644 --- a/js/src/wrappers/shared/scorers.ts +++ b/js/src/wrappers/shared/scorers.ts @@ -1,4 +1,4 @@ -import type { Span } from "../../logger"; +import type { Span } from "../../logger/core"; import type { Score } from "../../../util/score"; import type { ScorerFunction } from "./types"; import type { OneOrMoreScores } from "../../framework"; diff --git a/js/src/wrappers/shared/traced-eval.ts b/js/src/wrappers/shared/traced-eval.ts index c0e878559..9719b3913 100644 --- a/js/src/wrappers/shared/traced-eval.ts +++ b/js/src/wrappers/shared/traced-eval.ts @@ -1,4 +1,5 @@ -import { logError, type Experiment, type Span } from "../../logger"; +import { logError, type Span } from "../../logger/core"; +import { type Experiment } from "../../logger/experiments"; import { SpanTypeAttribute } from "../../../util/index"; import { runScorers } from "./scorers"; import type { ScorerFunction } from "./types"; diff --git a/js/src/wrappers/strands-agent-sdk.test.ts b/js/src/wrappers/strands-agent-sdk.test.ts index 66d2ad46c..469f16c24 100644 --- a/js/src/wrappers/strands-agent-sdk.test.ts +++ b/js/src/wrappers/strands-agent-sdk.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const { invoke } = vi.hoisted(() => ({ invoke: vi.fn( @@ -11,7 +11,7 @@ const { invoke } = vi.hoisted(() => ({ ), })); -vi.mock("../isomorph", () => ({ +vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(() => ({ invoke, diff --git a/js/src/wrappers/strands-agent-sdk.ts b/js/src/wrappers/strands-agent-sdk.ts index a05f57991..a905bfb89 100644 --- a/js/src/wrappers/strands-agent-sdk.ts +++ b/js/src/wrappers/strands-agent-sdk.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/strandsAgentSDK"; import { strandsAgentSDKChannels } from "../instrumentation/providers/strands-agent-sdk-channels"; import type { StrandsAgent, @@ -25,6 +26,7 @@ const WRAPPED_INSTANCE = Symbol.for( * typed instrumentation channels; the Strands plugin owns span lifecycle. */ export function wrapStrandsAgentSDK(sdk: T): T { + registerInstrumentation(); if (!sdk || typeof sdk !== "object") { return sdk; } diff --git a/js/src/wrappers/test_oai_attachments.test.ts b/js/src/wrappers/test_oai_attachments.test.ts index 443086900..42c23f191 100644 --- a/js/src/wrappers/test_oai_attachments.test.ts +++ b/js/src/wrappers/test_oai_attachments.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach } from "vite-plus/test"; import OpenAI from "openai"; import { wrapOpenAI } from "./oai"; -import { Attachment, initLogger } from "../logger"; +import { Attachment, initLogger } from "../logger/core"; const PROJECT_NAME = "test-project-openai-attachment-processing"; const TEST_MODEL = "gpt-4o-mini"; diff --git a/js/src/wrappers/typesafe.test.ts b/js/src/wrappers/typesafe.test.ts index 33e9d548d..faef67dc1 100644 --- a/js/src/wrappers/typesafe.test.ts +++ b/js/src/wrappers/typesafe.test.ts @@ -6,8 +6,9 @@ import { expect, it, vi, -} from "vitest"; -import { _exportsForTestingOnly, initLogger } from "../logger"; +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger } from "../logger/core"; import { configureNode } from "../node/config"; import type { TypeSafeAPIPromise, diff --git a/js/src/wrappers/typesafe.ts b/js/src/wrappers/typesafe.ts index 946a88884..894c3b6b3 100644 --- a/js/src/wrappers/typesafe.ts +++ b/js/src/wrappers/typesafe.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/typesafe"; import { typeSafeChannels } from "../instrumentation/providers/typesafe-channels"; import type { TypeSafeClient, @@ -7,6 +8,7 @@ import type { /** Wrap a TypeSafe client so systemOne calls pass through Braintrust tracing. */ export function wrapTypeSafe(client: T): T; export function wrapTypeSafe(client: unknown): unknown { + registerInstrumentation(); if (!isSupportedTypeSafeClient(client)) { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.warn("Unsupported TypeSafe AI library. Not wrapping."); diff --git a/js/src/wrappers/vitest-evals/reporter.test.ts b/js/src/wrappers/vitest-evals/reporter.test.ts index ccff70440..7ad2b9c72 100644 --- a/js/src/wrappers/vitest-evals/reporter.test.ts +++ b/js/src/wrappers/vitest-evals/reporter.test.ts @@ -1,9 +1,14 @@ -import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; -import BraintrustVitestEvalsReporter from "./reporter"; import { - _exportsForTestingOnly, - type TestBackgroundLogger, -} from "../../logger"; + beforeAll, + beforeEach, + describe, + expect, + test, + vi, +} from "vite-plus/test"; +import BraintrustVitestEvalsReporter from "./reporter"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { type TestBackgroundLogger } from "../../logger/core"; import * as logger from "../../logger"; import { configureNode } from "../../node/config"; diff --git a/js/src/wrappers/vitest-evals/reporter.ts b/js/src/wrappers/vitest-evals/reporter.ts index ad3de38be..1aa7189d0 100644 --- a/js/src/wrappers/vitest-evals/reporter.ts +++ b/js/src/wrappers/vitest-evals/reporter.ts @@ -1,6 +1,7 @@ import type { Reporter, TestCase, TestModule, Vitest } from "vitest/node"; import { SpanTypeAttribute, isObject } from "../../../util"; -import { init, logError, type Experiment, type Span } from "../../logger"; +import { init, type Experiment } from "../../logger/experiments"; +import { logError, type Span } from "../../logger/core"; import { configureNode } from "../../node/config"; import { summarizeAndFlush } from "../shared/flush"; diff --git a/js/src/wrappers/vitest/context-manager.ts b/js/src/wrappers/vitest/context-manager.ts index e80404dd3..aa45c295f 100644 --- a/js/src/wrappers/vitest/context-manager.ts +++ b/js/src/wrappers/vitest/context-manager.ts @@ -1,5 +1,6 @@ -import iso, { type IsoAsyncLocalStorage } from "../../isomorph"; -import type { Dataset, Experiment } from "../../logger"; +import iso, { type IsoAsyncLocalStorage } from "#braintrust-platform"; +import type { Dataset } from "../../logger/datasets"; +import type { Experiment } from "../../logger/experiments"; /** * VitestExperimentContext - State for a single describe block diff --git a/js/src/wrappers/vitest/expect-wrapper.ts b/js/src/wrappers/vitest/expect-wrapper.ts index c3207e6dc..e99765b36 100644 --- a/js/src/wrappers/vitest/expect-wrapper.ts +++ b/js/src/wrappers/vitest/expect-wrapper.ts @@ -1,5 +1,5 @@ -import { currentSpan } from "../../logger"; -import type { Span } from "../../logger"; +import { currentSpan } from "../../logger/core"; +import type { Span } from "../../logger/core"; function proxyAssertion( assertion: object, @@ -53,9 +53,9 @@ function proxyAssertion( }); } -export function wrapExpect unknown>( - originalExpect: ExpectType, -): ExpectType { +export function wrapExpect< + ExpectType extends (value: unknown, message?: string) => unknown, +>(originalExpect: ExpectType): ExpectType { const wrapped = function (value: unknown, message?: string) { // pass through unnamed expect if (message === undefined) { diff --git a/js/src/wrappers/vitest/index.ts b/js/src/wrappers/vitest/index.ts index 6e2c24fdd..9fa5acfa3 100644 --- a/js/src/wrappers/vitest/index.ts +++ b/js/src/wrappers/vitest/index.ts @@ -53,8 +53,9 @@ import type { VitestMethods, BraintrustVitest, WrapperConfig } from "./types"; */ export function wrapVitest< VitestContext = unknown, - ExpectType extends (...args: unknown[]) => unknown = ( - ...args: unknown[] + ExpectType extends (value: unknown, message?: string) => unknown = ( + value: unknown, + message?: string, ) => unknown, >( vitestMethods: VitestMethods, diff --git a/js/src/wrappers/vitest/package.json b/js/src/wrappers/vitest/package.json index e88dd0242..73dfde123 100644 --- a/js/src/wrappers/vitest/package.json +++ b/js/src/wrappers/vitest/package.json @@ -4,11 +4,12 @@ "description": "Test infrastructure for Vitest wrapper", "scripts": { "check:typings": "tsc --noEmit -p tsconfig.json", - "test": "vitest run" + "test": "vp test run" }, "devDependencies": { "@types/node": "^20.10.5", "typescript": "5.4.4", - "vitest": "4.1.11" + "vitest": "catalog:", + "vite-plus": "catalog:" } } diff --git a/js/src/wrappers/vitest/types.ts b/js/src/wrappers/vitest/types.ts index a587417e5..511908cda 100644 --- a/js/src/wrappers/vitest/types.ts +++ b/js/src/wrappers/vitest/types.ts @@ -1,4 +1,4 @@ -import type { Span } from "../../logger"; +import type { Span } from "../../logger/core"; import type { ScorerFunction, ProgressEvent } from "../shared/types"; @@ -89,8 +89,9 @@ export type WrappedDescribe = WithModifiers; export interface VitestMethods< VitestContext = unknown, - ExpectType extends (...args: unknown[]) => unknown = ( - ...args: unknown[] + ExpectType extends (value: unknown, message?: string) => unknown = ( + value: unknown, + message?: string, ) => unknown, > { test: TestFunction; @@ -105,8 +106,9 @@ export interface VitestMethods< export interface BraintrustVitest< VitestContext = unknown, - ExpectType extends (...args: unknown[]) => unknown = ( - ...args: unknown[] + ExpectType extends (value: unknown, message?: string) => unknown = ( + value: unknown, + message?: string, ) => unknown, > { test: WrappedTest; diff --git a/js/src/wrappers/vitest/vite.config.ts b/js/src/wrappers/vitest/vite.config.ts new file mode 100644 index 000000000..3a6974d0d --- /dev/null +++ b/js/src/wrappers/vitest/vite.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + resolve: { + alias: { + "#braintrust-platform": fileURLToPath( + new URL("../../isomorph.ts", import.meta.url), + ), + }, + }, + test: { + hookTimeout: 60_000, + reporters: ["default"], + }, +}); diff --git a/js/src/wrappers/vitest/vitest-wrapper-example.test.ts b/js/src/wrappers/vitest/vitest-wrapper-example.test.ts index 452e6e8ca..538d343b7 100644 --- a/js/src/wrappers/vitest/vitest-wrapper-example.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper-example.test.ts @@ -1,8 +1,8 @@ -import * as vitest from "vitest"; +import * as vitest from "vite-plus/test"; import { configureNode } from "../../node/config"; import { wrapVitest } from "./index"; -import { _exportsForTestingOnly } from "../../logger"; -import * as logger from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import * as experiments from "../../logger/experiments"; configureNode(); @@ -11,7 +11,7 @@ await _exportsForTestingOnly.simulateLoginForTests(); const moduleBackgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); vitest.vi - .spyOn(logger, "init") + .spyOn(experiments, "init") .mockImplementation((projectName: string, options?: any) => { return _exportsForTestingOnly.initTestExperiment( options?.experiment || "test-experiment", diff --git a/js/src/wrappers/vitest/vitest-wrapper-span.test.ts b/js/src/wrappers/vitest/vitest-wrapper-span.test.ts index a7995b20a..b062cfd5d 100644 --- a/js/src/wrappers/vitest/vitest-wrapper-span.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper-span.test.ts @@ -1,8 +1,8 @@ -import * as vitest from "vitest"; +import * as vitest from "vite-plus/test"; import { configureNode } from "../../node/config"; import { wrapVitest } from "./index"; -import { _exportsForTestingOnly } from "../../logger"; -import * as logger from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import * as experiments from "../../logger/experiments"; configureNode(); @@ -11,7 +11,7 @@ await _exportsForTestingOnly.simulateLoginForTests(); const moduleBackgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); vitest.vi - .spyOn(logger, "init") + .spyOn(experiments, "init") .mockImplementation((projectName: string, options?: any) => { return _exportsForTestingOnly.initTestExperiment( options?.experiment || "test-experiment", diff --git a/js/src/wrappers/vitest/vitest-wrapper.test.ts b/js/src/wrappers/vitest/vitest-wrapper.test.ts index d3d7bed6f..36b78a2d0 100644 --- a/js/src/wrappers/vitest/vitest-wrapper.test.ts +++ b/js/src/wrappers/vitest/vitest-wrapper.test.ts @@ -8,16 +8,16 @@ import { afterEach, vi, expectTypeOf, -} from "vitest"; +} from "vite-plus/test"; import { configureNode } from "../../node/config"; import { wrapVitest } from "./index"; import { wrapExpect } from "./expect-wrapper"; -import { - _exportsForTestingOnly, - type TestBackgroundLogger, -} from "../../logger"; +import { _exportsForTestingOnly } from "../../logger/testing"; +import { type TestBackgroundLogger } from "../../logger/core"; import type { BraintrustVitest } from "./types"; -import * as logger from "../../logger"; +import * as experiments from "../../logger/experiments"; +import * as logger from "../../logger/core"; +import * as datasets from "../../logger/datasets"; import { getExperimentContext } from "./wrapper"; import { getVitestContextManager, @@ -33,12 +33,12 @@ await _exportsForTestingOnly.simulateLoginForTests(); _exportsForTestingOnly.useTestBackgroundLogger(); // ✅ STEP 2: Mock initDataset and init to avoid network calls -vi.spyOn(logger, "initDataset").mockReturnValue({ +vi.spyOn(datasets, "initDataset").mockReturnValue({ insert: vi.fn(() => "test-example-id"), } as any); const initSpy = vi - .spyOn(logger, "init") + .spyOn(experiments, "init") .mockImplementation((projectOrOptions: string | any, options?: any) => { const experimentOptions = typeof projectOrOptions === "string" ? options : projectOrOptions; diff --git a/js/src/wrappers/vitest/vitest.config.js b/js/src/wrappers/vitest/vitest.config.js deleted file mode 100644 index c6b85f3bf..000000000 --- a/js/src/wrappers/vitest/vitest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - hookTimeout: 60_000, - reporters: ["default"], - }, -}); diff --git a/js/src/wrappers/vitest/wrapper.ts b/js/src/wrappers/vitest/wrapper.ts index ce7a34e4d..7ec5e5bf0 100644 --- a/js/src/wrappers/vitest/wrapper.ts +++ b/js/src/wrappers/vitest/wrapper.ts @@ -1,4 +1,4 @@ -import { init } from "../../logger"; +import { init } from "../../logger/experiments"; import type { TestConfig, TestContext, diff --git a/js/src/wrappers/voyageai.test.ts b/js/src/wrappers/voyageai.test.ts index e9a1f9e19..688d4d39f 100644 --- a/js/src/wrappers/voyageai.test.ts +++ b/js/src/wrappers/voyageai.test.ts @@ -6,8 +6,9 @@ import { expect, it, vi, -} from "vitest"; -import { _exportsForTestingOnly, initLogger } from "../logger"; +} from "vite-plus/test"; +import { _exportsForTestingOnly } from "../logger/testing"; +import { initLogger } from "../logger/core"; import { configureNode } from "../node/config"; import { wrapVoyageAI } from "./voyageai"; diff --git a/js/src/wrappers/voyageai.ts b/js/src/wrappers/voyageai.ts index 8285282f5..08d84a44f 100644 --- a/js/src/wrappers/voyageai.ts +++ b/js/src/wrappers/voyageai.ts @@ -1,3 +1,4 @@ +import { registerInstrumentation } from "../instrumentation/registrations/voyageai"; import { voyageAIChannels } from "../instrumentation/providers/voyageai-channels"; import type { VoyageAIClient, @@ -12,6 +13,7 @@ import type { * instrumentation hooks. */ export function wrapVoyageAI(client: T): T { + registerInstrumentation(); if (!isSupportedVoyageAIClient(client)) { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.warn("Unsupported Voyage AI library. Not wrapping."); diff --git a/js/src/zod/utils.ts b/js/src/zod/utils.ts index 2a36ef498..c67c1eee8 100644 --- a/js/src/zod/utils.ts +++ b/js/src/zod/utils.ts @@ -15,7 +15,9 @@ function isZodV4(zodObject: ZodSchema): zodObject is z4.ZodType { ); } -export function zodToJsonSchema(schema: ZodSchema) { +export function zodToJsonSchema( + schema: ZodSchema, +): ReturnType | ReturnType { if (isZodV4(schema)) { return z4.toJSONSchema(schema, { target: "draft-7", diff --git a/js/tests/api-compatibility/api-compatibility.test.ts b/js/tests/api-compatibility/api-compatibility.test.ts index 0b32fb552..a56fb7561 100644 --- a/js/tests/api-compatibility/api-compatibility.test.ts +++ b/js/tests/api-compatibility/api-compatibility.test.ts @@ -1,11 +1,10 @@ -import { describe, test, expect, beforeAll, afterAll } from "vitest"; +import { describe, test, expect, beforeAll, afterAll } from "vite-plus/test"; import { execSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import * as ts from "typescript"; import * as tar from "tar"; -import type { Options } from "tsup"; /** * API Compatibility Test @@ -47,73 +46,46 @@ import type { Options } from "tsup"; * and handles complex TypeScript syntax correctly. */ -/** - * Extracts entrypoints from tsup.config.ts - * Reads the config and identifies all builds that generate .d.ts files - */ -async function getEntrypointsFromTsupConfig(): Promise< - Array<{ name: string; typesPath: string }> -> { - const configPath = path.join(__dirname, "..", "..", "tsup.config.ts"); - - // Dynamically import the tsup config - const configModule = await import(configPath); - const config: Options | Options[] = configModule.default; - - const configs = Array.isArray(config) ? config : [config]; - const entrypoints: Array<{ name: string; typesPath: string }> = []; - - for (const cfg of configs) { - // Skip configs without dts enabled - if (!cfg.dts) continue; - - const entry = cfg.entry; - const outDir = cfg.outDir || "dist"; - - if (Array.isArray(entry)) { - for (const entryFile of entry) { - const name = getEntrypointName(entryFile, outDir); - const typesPath = getTypesPath(entryFile, outDir); - entrypoints.push({ name, typesPath }); - } - } else if (typeof entry === "object") { - // Object entry keys determine the emitted path, including nested paths. - for (const key of Object.keys(entry)) { - const name = key; - const typesPath = path.join(outDir, `${key}.d.ts`); - entrypoints.push({ name, typesPath }); +interface Entrypoint { + name: string; + typesPath: string; + runtimePath: string; +} + +/** Read paths from each package's own manifest, since layouts change between releases. */ +function getEntrypointsFromPackageExports(packageRoot: string): Entrypoint[] { + const pkg = JSON.parse( + fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"), + ); + function target( + value: unknown, + condition: "types" | "runtime", + ): string | undefined { + if (typeof value === "string") return value; + if (!value || typeof value !== "object") return undefined; + const conditions = value as Record; + for (const key of condition === "types" + ? ["types", "node", "import", "default"] + : ["node", "import", "default"]) { + if (conditions[key] !== undefined) { + const resolved = target(conditions[key], condition); + if (resolved) return resolved; } } + return undefined; } - - return entrypoints; -} - -/** - * Determines the entrypoint name from the entry file path - */ -function getEntrypointName(entryFile: string, outDir: string): string { - const basename = path.basename(entryFile, path.extname(entryFile)); - - // Map common patterns to friendly names - if (entryFile.includes("src/node/index.ts")) return "main"; - if (entryFile.includes("src/edge-light/index.ts")) return "edge-light"; - if (entryFile.includes("src/workerd/index.ts")) return "workerd"; - - // Default to basename - return basename; -} - -/** - * Constructs the .d.ts path from entry file and output directory - */ -function getTypesPath(entryFile: string, outDir: string): string { - const basename = path.basename(entryFile, path.extname(entryFile)); - return path.join(outDir, `${basename}.d.ts`); + return Object.entries(pkg.exports).flatMap(([name, entry]) => { + const typesPath = target(entry, "types"); + const runtimePath = target(entry, "runtime"); + if (!typesPath || !/\.d\.[cm]?ts$/.test(typesPath) || !runtimePath) + return []; + return [ + { name: name === "." ? "main" : name.slice(2), typesPath, runtimePath }, + ]; + }); } -// Will be populated in beforeAll -let ENTRYPOINTS: Array<{ name: string; typesPath: string }> = []; +let ENTRYPOINTS: Entrypoint[] = []; interface VersionInfo { major: number; @@ -194,144 +166,35 @@ function getChangesetBumpType( * of each symbol to get its true signature, rather than using the export statement. */ function extractExportsFromDts(filePath: string): Map { - const exports = new Map(); - - if (!fs.existsSync(filePath)) { - return exports; - } - - const sourceText = fs.readFileSync(filePath, "utf-8"); - const sourceFile = ts.createSourceFile( - filePath, - sourceText, - ts.ScriptTarget.Latest, - true, + const program = ts.createProgram([filePath], { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + }); + const checker = program.getTypeChecker(); + const source = program.getSourceFile(filePath); + if (!source) throw new Error(`Missing declarations: ${filePath}`); + const module = checker.getSymbolAtLocation(source); + if (!module) throw new Error(`No module exports in ${filePath}`); + return new Map( + checker.getExportsOfModule(module).map((exported) => { + const symbol = + exported.flags & ts.SymbolFlags.Alias + ? checker.getAliasedSymbol(exported) + : exported; + const declarations = symbol.declarations ?? []; + const declaration = declarations[0]; + return [ + exported.name, + { + name: exported.name, + kind: declaration ? getNodeKind(declaration) : "re-export", + signature: declarations.map((node) => node.getText()).join("\n"), + }, + ]; + }), ); - - // First pass: collect all declarations by name - const declarations = new Map(); - - function collectDeclarations(node: ts.Node) { - let name: string | undefined; - - if (ts.isFunctionDeclaration(node) && node.name) { - name = node.name.text; - } else if (ts.isClassDeclaration(node) && node.name) { - name = node.name.text; - } else if (ts.isInterfaceDeclaration(node)) { - name = node.name.text; - } else if (ts.isTypeAliasDeclaration(node)) { - name = node.name.text; - } else if (ts.isEnumDeclaration(node)) { - name = node.name.text; - } else if (ts.isVariableStatement(node)) { - node.declarationList.declarations.forEach((decl) => { - if (ts.isIdentifier(decl.name)) { - declarations.set(decl.name.text, node); - } - }); - } else if (ts.isModuleDeclaration(node) && node.name) { - name = node.name.text; - } - - if (name) { - declarations.set(name, node); - } - - ts.forEachChild(node, collectDeclarations); - } - - collectDeclarations(sourceFile); - - // Helper to check if a node is inside a namespace - function isInsideNamespace(node: ts.Node): boolean { - let parent = node.parent; - while (parent) { - if (ts.isModuleDeclaration(parent)) { - return true; - } - parent = parent.parent; - } - return false; - } - - // Second pass: process exports - function visit(node: ts.Node) { - // Skip exports inside namespaces - they're not top-level exports - if (isInsideNamespace(node)) { - ts.forEachChild(node, visit); - return; - } - - // Handle: export { foo, bar } or export { foo, bar } from './module' - if (ts.isExportDeclaration(node)) { - if (node.exportClause && ts.isNamedExports(node.exportClause)) { - node.exportClause.elements.forEach((element) => { - const name = element.name.text; - const declaration = declarations.get(name); - - exports.set(name, { - name, - kind: declaration ? getNodeKind(declaration) : "re-export", - // Use the actual declaration's signature if found, otherwise just the name - signature: declaration - ? declaration.getText(sourceFile) - : `export { ${name} }`, - }); - }); - } - } else if ( - ts.canHaveModifiers(node) && - ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) - ) { - // Handle direct exports with 'export' keyword - let name: string | undefined; - let kind: string = "unknown"; - const signature: string = node.getText(sourceFile); - - if (ts.isFunctionDeclaration(node) && node.name) { - name = node.name.text; - kind = "function"; - } else if (ts.isClassDeclaration(node) && node.name) { - name = node.name.text; - kind = "class"; - } else if (ts.isInterfaceDeclaration(node)) { - name = node.name.text; - kind = "interface"; - } else if (ts.isTypeAliasDeclaration(node)) { - name = node.name.text; - kind = "type"; - } else if (ts.isEnumDeclaration(node)) { - name = node.name.text; - kind = "enum"; - } else if (ts.isVariableStatement(node)) { - // Handle: export const foo = ... - node.declarationList.declarations.forEach((decl) => { - if (ts.isIdentifier(decl.name)) { - const varName = decl.name.text; - exports.set(varName, { - name: varName, - kind: "variable", - signature: node.getText(sourceFile), - }); - } - }); - return; // Don't add to exports again below - } else if (ts.isModuleDeclaration(node) && node.name) { - name = node.name.text; - kind = "namespace"; - } - - if (name) { - exports.set(name, { name, kind, signature }); - } - } - - ts.forEachChild(node, visit); - } - - visit(sourceFile); - return exports; } function getNodeKind(node: ts.Node): string { @@ -340,7 +203,8 @@ function getNodeKind(node: ts.Node): string { if (ts.isInterfaceDeclaration(node)) return "interface"; if (ts.isTypeAliasDeclaration(node)) return "type"; if (ts.isEnumDeclaration(node)) return "enum"; - if (ts.isVariableStatement(node)) return "variable"; + if (ts.isVariableStatement(node) || ts.isVariableDeclaration(node)) + return "variable"; if (ts.isModuleDeclaration(node)) return "namespace"; return "unknown"; } @@ -348,26 +212,22 @@ function getNodeKind(node: ts.Node): string { /** * Extracts actual runtime exports from a JavaScript module file */ -async function extractRuntimeExports(mjsPath: string): Promise> { - const exports = new Set(); - - if (!fs.existsSync(mjsPath)) { - return exports; - } - - try { - // Dynamically import the module - const module = await import(mjsPath); - - // Get all exported names - for (const key of Object.keys(module)) { - exports.add(key); - } - } catch (error) { - console.warn(`Failed to load runtime exports from ${mjsPath}:`, error); - } - - return exports; +async function extractRuntimeExports(modulePath: string): Promise> { + // Inspect ESM exports without executing setup entrypoints or test-runner integrations. + const program = ts.createProgram([modulePath], { + allowJs: true, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + target: ts.ScriptTarget.ES2022, + }); + const source = program.getSourceFile(modulePath); + if (!source) throw new Error(`Missing runtime entrypoint: ${modulePath}`); + const checker = program.getTypeChecker(); + const module = checker.getSymbolAtLocation(source); + if (!module) throw new Error(`No runtime module in ${modulePath}`); + return new Set( + checker.getExportsOfModule(module).map((symbol) => symbol.name), + ); } /** @@ -2742,44 +2602,20 @@ describe("areConstArraySignaturesCompatible", () => { * Since dist/ files aren't in git, we need to build main first in CI. * This checks if a pre-built baseline exists from CI, otherwise returns null. */ -async function getMainBranchExports(entrypoint: { - name: string; - typesPath: string; -}): Promise | null> { - try { - // Check if we're in CI and have a baseline directory - const baselineDir = - process.env.BASELINE_DIR || path.join(os.tmpdir(), "braintrust-baseline"); - const baselinePath = path.join(baselineDir, "js", entrypoint.typesPath); - - if (fs.existsSync(baselinePath)) { - // Baseline exists (built by CI) - console.log(`${entrypoint.name}: Using baseline from ${baselinePath}`); - const exports = extractExportsFromDts(baselinePath); - return exports; - } - - // Fallback: try to read from git (won't work for .d.ts but good for testing) - const filePath = `js/${entrypoint.typesPath}`; - const content = execSync(`git show origin/main:${filePath}`, { - encoding: "utf-8", - stdio: ["pipe", "pipe", "ignore"], // Suppress stderr - }); - - // Parse the content as if it were a file - const tempFile = path.join( - os.tmpdir(), - `main-${entrypoint.name}-${Date.now()}.d.ts`, - ); - fs.writeFileSync(tempFile, content); - const exports = extractExportsFromDts(tempFile); - fs.unlinkSync(tempFile); - - return exports; - } catch (error) { - // Baseline not available - will fall back to direct comparison - return null; - } +async function getMainBranchExports( + entrypoint: Entrypoint, +): Promise | null> { + const baselineRoot = path.join( + process.env.BASELINE_DIR || path.join(os.tmpdir(), "braintrust-baseline"), + "js", + ); + if (!fs.existsSync(path.join(baselineRoot, "package.json"))) return null; + const baseline = getEntrypointsFromPackageExports(baselineRoot).find( + ({ name }) => name === entrypoint.name, + ); + return baseline + ? extractExportsFromDts(path.join(baselineRoot, baseline.typesPath)) + : null; } interface BreakingChanges { @@ -2865,10 +2701,12 @@ describe("API Compatibility", () => { let versionBumpType: "major" | "minor" | "patch" | "none"; beforeAll(async () => { - // Load entrypoints from tsup config - ENTRYPOINTS = await getEntrypointsFromTsupConfig(); + // Load entrypoints from the package contract + ENTRYPOINTS = getEntrypointsFromPackageExports( + path.join(__dirname, "../.."), + ); console.log( - `Loaded ${ENTRYPOINTS.length} entrypoints from tsup.config.ts:`, + `Loaded ${ENTRYPOINTS.length} entrypoints from package.json:`, ENTRYPOINTS.map((e) => e.name).join(", "), ); @@ -2966,7 +2804,7 @@ describe("API Compatibility", () => { } const mainDeclaration = fs.readFileSync( - path.join(declarationRoot, "dist/index.d.ts"), + path.join(declarationRoot, "dist/node/index.d.ts"), "utf8", ); for (const schemaName of [ @@ -3009,11 +2847,12 @@ describe("API Compatibility", () => { const failures: string[] = []; for (const entrypoint of ENTRYPOINTS) { - const publishedTypesPath = path.join( - tempDir, - "package", - entrypoint.typesPath, + const publishedRoot = path.join(tempDir, "package"); + const published = getEntrypointsFromPackageExports(publishedRoot).find( + ({ name }) => name === entrypoint.name, ); + if (!published) continue; // A new subpath has no previous contract to compare. + const publishedTypesPath = path.join(publishedRoot, published.typesPath); const currentTypesPath = path.join( __dirname, "..", @@ -3023,10 +2862,9 @@ describe("API Compatibility", () => { // Check if both files exist if (!fs.existsSync(publishedTypesPath)) { - console.warn( - `Published types not found: ${publishedTypesPath}. Skipping ${entrypoint.name}.`, + throw new Error( + `Published declarations are missing: ${publishedTypesPath}`, ); - continue; } if (!fs.existsSync(currentTypesPath)) { @@ -3044,9 +2882,11 @@ describe("API Compatibility", () => { ); // Verify that current runtime exports match .d.ts declarations - const currentMjsPath = path - .join(__dirname, "..", entrypoint.typesPath) - .replace(/\.d\.ts$/, ".mjs"); + const currentMjsPath = path.join( + __dirname, + "../..", + entrypoint.runtimePath, + ); if (fs.existsSync(currentMjsPath)) { const runtimeExports = await extractRuntimeExports(currentMjsPath); const runtimeErrors = verifyRuntimeMatchesTypes( diff --git a/js/tests/auto-instrumentations/bundled-runtime.test.ts b/js/tests/auto-instrumentations/bundled-runtime.test.ts index b958c498a..d440f7dfb 100644 --- a/js/tests/auto-instrumentations/bundled-runtime.test.ts +++ b/js/tests/auto-instrumentations/bundled-runtime.test.ts @@ -11,7 +11,14 @@ * by transformation.test.ts. */ -import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; +import { + describe, + it, + expect, + beforeAll, + afterAll, + afterEach, +} from "vite-plus/test"; import { EventEmitter } from "node:events"; import * as fs from "node:fs"; import * as path from "node:path"; diff --git a/js/tests/auto-instrumentations/import-require-in-the-middle.test.ts b/js/tests/auto-instrumentations/import-require-in-the-middle.test.ts index 551c0804f..f53b288c4 100644 --- a/js/tests/auto-instrumentations/import-require-in-the-middle.test.ts +++ b/js/tests/auto-instrumentations/import-require-in-the-middle.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { spawn } from "node:child_process"; import * as path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; diff --git a/js/tests/auto-instrumentations/loader-hook.test.ts b/js/tests/auto-instrumentations/loader-hook.test.ts index 688e1aa97..c2802ac9a 100644 --- a/js/tests/auto-instrumentations/loader-hook.test.ts +++ b/js/tests/auto-instrumentations/loader-hook.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { describe, it, expect, beforeAll, afterAll } from "vite-plus/test"; import { Worker } from "node:worker_threads"; import * as fs from "node:fs"; import * as os from "node:os"; @@ -10,10 +10,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesDir = path.join(__dirname, "fixtures"); // Path to unified loader hook (built dist file) -const hookPath = path.join( - __dirname, - "../../dist/auto-instrumentations/hook.mjs", -); +const hookPath = path.join(__dirname, "../../dist/hooks/hook.mjs"); // Paths to fixtures const listenerPath = path.join(fixturesDir, "listener-esm.mjs"); diff --git a/js/tests/auto-instrumentations/next-config.test.ts b/js/tests/auto-instrumentations/next-config.test.ts index 8d1189068..c88f01086 100644 --- a/js/tests/auto-instrumentations/next-config.test.ts +++ b/js/tests/auto-instrumentations/next-config.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; const { requireFromProject } = vi.hoisted(() => ({ requireFromProject: Object.assign(vi.fn(), { diff --git a/js/tests/auto-instrumentations/orchestrion-js-upstream.test.ts b/js/tests/auto-instrumentations/orchestrion-js-upstream.test.ts index cdcc38b78..76d363333 100644 --- a/js/tests/auto-instrumentations/orchestrion-js-upstream.test.ts +++ b/js/tests/auto-instrumentations/orchestrion-js-upstream.test.ts @@ -3,7 +3,7 @@ * licensed under Apache-2.0. Modified by Braintrust. */ -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test"; import { spawnSync } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; diff --git a/js/tests/auto-instrumentations/test-bundle.ts b/js/tests/auto-instrumentations/test-bundle.ts index 14e70a26b..7b863db6f 100644 --- a/js/tests/auto-instrumentations/test-bundle.ts +++ b/js/tests/auto-instrumentations/test-bundle.ts @@ -44,7 +44,7 @@ export async function bundleFixture( lib: { entry: entryPoint, formats: ["es"], - fileName: path.basename(outfile, ".mjs"), + fileName: () => path.basename(outfile), }, outDir: path.dirname(outfile), emptyOutDir: false, diff --git a/js/tests/auto-instrumentations/transformation.test.ts b/js/tests/auto-instrumentations/transformation.test.ts index 59e83d11c..ec6ec4c03 100644 --- a/js/tests/auto-instrumentations/transformation.test.ts +++ b/js/tests/auto-instrumentations/transformation.test.ts @@ -7,7 +7,7 @@ * IMPORTANT: Tests use a mock OpenAI package structure in test/fixtures/node_modules/openai. */ -import { describe, it, expect, beforeAll, afterAll, vi } from "vitest"; +import { describe, it, expect, beforeAll, afterAll, vi } from "vite-plus/test"; import * as esbuild from "esbuild"; import * as fs from "node:fs"; import * as path from "node:path"; @@ -719,7 +719,7 @@ describe("Orchestrion Transformation Tests", () => { describe("turbopack / webpack loader", () => { const webpackLoaderPath = path.resolve( __dirname, - "../../dist/auto-instrumentations/bundler/webpack-loader.cjs", + "../../dist/loaders/webpack-loader.cjs", ); async function runWebpackWithLoader( config: object, diff --git a/js/tests/provider-wrappers.test.ts b/js/tests/provider-wrappers.test.ts index ee4ce6363..17b807b69 100644 --- a/js/tests/provider-wrappers.test.ts +++ b/js/tests/provider-wrappers.test.ts @@ -7,7 +7,7 @@ import { expect, test, vi, -} from "vitest"; +} from "vite-plus/test"; import OpenAI from "openai"; import { configureNode } from "../src/node/config"; import { _exportsForTestingOnly, Attachment, initLogger } from "../src/logger"; diff --git a/js/tsconfig.json b/js/tsconfig.json index de86073ac..97aab6f5a 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -2,14 +2,14 @@ "compilerOptions": { "declaration": true, "lib": ["es2022"], - "module": "commonjs", + "module": "ESNext", "target": "es2022", - "moduleResolution": "node", - "baseUrl": ".", + "moduleResolution": "Bundler", "paths": { "@apm-js-collab/code-transformer": [ - "src/auto-instrumentations/orchestrion-js/index.ts" - ] + "./src/auto-instrumentations/orchestrion-js/index.ts" + ], + "#braintrust-platform": ["./src/isomorph.ts"] }, "strict": true, "noUnusedLocals": true, @@ -25,14 +25,13 @@ "**/smoke/**", "scripts/**", "vendor/**", - "eslint.config.ts", - "tsup.config.ts", "src/auto-instrumentations/**/*.mts", "src/auto-instrumentations/bundler/**", "src/auto-instrumentations/configs/**", "src/auto-instrumentations/loader/**", "src/auto-instrumentations/index.ts", "tests/auto-instrumentations/**", - "./playground.ts" + "./playground.ts", + "vite.config.ts" ] } diff --git a/js/tsconfig.test.json b/js/tsconfig.test.json index 0bcb4a371..50e053f02 100644 --- a/js/tsconfig.test.json +++ b/js/tsconfig.test.json @@ -5,14 +5,15 @@ "module": "es2022", "moduleResolution": "bundler", "noEmit": true, - "types": ["node", "vitest/globals"], + "types": ["node", "vite-plus/test/globals"], "paths": { "@apm-js-collab/code-transformer": [ - "src/auto-instrumentations/orchestrion-js/index.ts" + "./src/auto-instrumentations/orchestrion-js/index.ts" ], - "@ai-sdk/openai": ["tests/type-shims/ai-sdk-openai.d.ts"], - "ai": ["tests/type-shims/ai.d.ts"], - "braintrust": ["src/node/index.ts"] + "@ai-sdk/openai": ["./tests/type-shims/ai-sdk-openai.d.ts"], + "ai": ["./tests/type-shims/ai.d.ts"], + "braintrust": ["./src/node/index.ts"], + "#braintrust-platform": ["./src/isomorph.ts"] } }, "include": [ diff --git a/js/tsup.config.ts b/js/tsup.config.ts deleted file mode 100644 index 1ff73edae..000000000 --- a/js/tsup.config.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { defineConfig } from "tsup"; -import { readFileSync } from "node:fs"; - -const packageJson = JSON.parse( - readFileSync(new URL("./package.json", import.meta.url), "utf8"), -) as { version: string }; - -const define = { - __BRAINTRUST_SDK_VERSION__: JSON.stringify(packageJson.version), -}; - -export default defineConfig([ - // Node.js entrypoint - { - entry: { - index: "src/node/index.ts", - "instrumentation/index": "src/instrumentation/index.ts", - "custom-views": "src/custom-views/exports.ts", - preprocessors: "src/preprocessors/exports.ts", - "apply-auto-instrumentation": - "src/node/apply-auto-instrumentation-entry.ts", - "vitest-evals-reporter": "src/wrappers/vitest-evals/reporter.ts", - }, - format: ["cjs", "esm"], - outDir: "dist", - external: ["zod"], - removeNodeProtocol: false, - dts: { - // Split DTS generation to reduce memory usage - compilerOptions: { - skipLibCheck: true, - }, - }, - splitting: true, - define, - clean: true, - }, - // Browser/edge entrypoints - { - entry: { - browser: "src/browser/index.ts", - "edge-light": "src/edge-light/index.ts", - workerd: "src/workerd/index.ts", - "apply-auto-instrumentation.browser": - "src/non-node/apply-auto-instrumentation-entry.ts", - }, - format: ["cjs", "esm"], - outDir: "dist", - external: ["zod"], - removeNodeProtocol: false, - platform: "browser", - splitting: false, - dts: true, - define, - clean: false, - }, - { - entry: [ - "src/auto-instrumentations/index.ts", - "src/auto-instrumentations/loader/cjs-patch.ts", - "src/auto-instrumentations/loader/get-package-version.ts", - "src/auto-instrumentations/bundler/vite.ts", - "src/auto-instrumentations/bundler/webpack.ts", - "src/auto-instrumentations/bundler/next.ts", - "src/auto-instrumentations/bundler/esbuild.ts", - "src/auto-instrumentations/bundler/rollup.ts", - ], - format: ["cjs", "esm"], - outDir: "dist/auto-instrumentations", - dts: true, - external: ["zod"], - define, - outExtension({ format }) { - if (format === "esm") { - return { js: ".mjs" }; - } - return { js: ".cjs" }; - }, - clean: true, - }, - { - entry: { - "bundler/webpack-loader": - "src/auto-instrumentations/bundler/webpack-loader.ts", - }, - format: ["cjs"], - outDir: "dist/auto-instrumentations", - dts: true, - external: ["zod"], - define, - outExtension() { - return { js: ".cjs" }; - }, - clean: false, - }, - { - entry: [ - "src/auto-instrumentations/hook.mts", - "src/auto-instrumentations/loader/esm-hook.mts", - ], - format: ["esm"], - outDir: "dist/auto-instrumentations", - dts: false, - platform: "node", - external: ["@anthropic-ai/sdk", "zod"], - define, - outExtension({ format }) { - return { js: ".mjs" }; - }, - clean: false, - }, -]); diff --git a/js/util/json_util.test.ts b/js/util/json_util.test.ts index e85470f88..48cf18e8e 100644 --- a/js/util/json_util.test.ts +++ b/js/util/json_util.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { deterministicReplacer } from "./json_util"; test("deterministicReplacer basic", () => { diff --git a/js/util/json_util.ts b/js/util/json_util.ts index dd81d6e73..1f4c0f20c 100644 --- a/js/util/json_util.ts +++ b/js/util/json_util.ts @@ -2,7 +2,7 @@ // serialized in a deterministic key order. // // https://gist.github.com/davidfurlong/463a83a33b70a3b6618e97ec9679e490 -export function deterministicReplacer(_key: string, value: any) { +export function deterministicReplacer(_key: string, value: any): any { return value instanceof Object && !(value instanceof Array) ? Object.keys(value) .sort() diff --git a/js/util/merge_row_batch.test.ts b/js/util/merge_row_batch.test.ts index 9465bf712..1baaa629e 100644 --- a/js/util/merge_row_batch.test.ts +++ b/js/util/merge_row_batch.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from "vite-plus/test"; import { mergeRowBatch, batchItems } from "./merge_row_batch"; import { IS_MERGE_FIELD } from "./db_fields"; diff --git a/js/util/object_util.test.ts b/js/util/object_util.test.ts index 794f601d6..f55875771 100644 --- a/js/util/object_util.test.ts +++ b/js/util/object_util.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe, afterEach } from "vitest"; +import { expect, test, describe, afterEach } from "vite-plus/test"; import { mapAt, forEachMissingKey, diff --git a/js/util/object_util.ts b/js/util/object_util.ts index 607bacbb8..ad9ea72d9 100644 --- a/js/util/object_util.ts +++ b/js/util/object_util.ts @@ -22,7 +22,7 @@ export function mergeDictsWithPaths({ mergeInto: Record; mergeFrom: Record; mergePaths: string[][]; -}) { +}): Record { const mergePathsSerialized = new Set( mergePaths.map((p) => JSON.stringify(p)), ); @@ -95,7 +95,7 @@ function mergeDictsWithPathsHelper({ export function mergeDicts( mergeInto: Record, mergeFrom: Record, -) { +): Record { return mergeDictsWithPaths({ mergeInto, mergeFrom, mergePaths: [] }); } @@ -114,7 +114,7 @@ export function forEachMissingKey({ v: unknown; path: string[]; }) => void; -}) { +}): void { function helper(lhs: unknown, rhs: unknown, path: string[]) { if (lhs instanceof Object) { if (!(rhs instanceof Object)) { diff --git a/js/util/span_identifier_v1.ts b/js/util/span_identifier_v1.ts index 50d6289e8..8c313a859 100644 --- a/js/util/span_identifier_v1.ts +++ b/js/util/span_identifier_v1.ts @@ -47,7 +47,7 @@ export class SpanRowIdsV1 { } } - public toObject() { + public toObject(): { rowId: string; spanId: string; rootSpanId: string } { return { rowId: this.rowId, spanId: this.spanId, @@ -160,7 +160,11 @@ export class SpanComponentsV1 { } } - public toObject() { + public toObject(): { + objectType: SpanObjectTypeV1; + objectId: string; + rowIds: { rowId: string; spanId: string; rootSpanId: string } | undefined; + } { return { objectType: this.objectType, objectId: this.objectId, diff --git a/js/util/span_identifier_v2.ts b/js/util/span_identifier_v2.ts index 2e37eb053..69c38b3fc 100644 --- a/js/util/span_identifier_v2.ts +++ b/js/util/span_identifier_v2.ts @@ -50,7 +50,7 @@ export class SpanRowIdsV2 { } } - public toObject() { + public toObject(): { rowId: string; spanId: string; rootSpanId: string } { return { rowId: this.rowId, spanId: this.spanId, @@ -244,7 +244,12 @@ export class SpanComponentsV2 { } } - public toObject() { + public toObject(): { + objectType: SpanObjectTypeV2; + objectId: string | undefined; + computeObjectMetadataArgs: Record | undefined; + rowIds: { rowId: string; spanId: string; rootSpanId: string } | undefined; + } { return { objectType: this.objectType, objectId: this.objectId, diff --git a/js/util/span_identifier_v4.test.ts b/js/util/span_identifier_v4.test.ts index 6084607d8..3c4fa4620 100644 --- a/js/util/span_identifier_v4.test.ts +++ b/js/util/span_identifier_v4.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe } from "vitest"; +import { expect, test, describe } from "vite-plus/test"; import { SpanComponentsV4, SpanComponentsV4Data, diff --git a/js/util/string_util.test.ts b/js/util/string_util.test.ts index f92af00eb..d31bf2b76 100644 --- a/js/util/string_util.test.ts +++ b/js/util/string_util.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { camelToSnakeCase, _urljoin } from "./string_util"; test("_urljoin", () => { diff --git a/js/util/string_util.ts b/js/util/string_util.ts index 079f629ce..b2256b8f3 100644 --- a/js/util/string_util.ts +++ b/js/util/string_util.ts @@ -7,32 +7,32 @@ export function _urljoin(...parts: string[]): string { .join("/"); } -export function capitalize(s: string, sep?: string) { +export function capitalize(s: string, sep?: string): string { const items = sep ? s.split(sep) : [s]; return items .map((s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s)) .join(sep || ""); } -export function lowercase(s: string, sep?: string) { +export function lowercase(s: string, sep?: string): string { const items = sep ? s.split(sep) : [s]; return items .map((s) => (s ? s.charAt(0).toLowerCase() + s.slice(1) : s)) .join(sep || ""); } -export function snakeToCamelCase(s: string) { +export function snakeToCamelCase(s: string): string { return s .split("_") .map((s) => capitalize(s)) .join(""); } -export function snakeToTitleCase(s: string) { +export function snakeToTitleCase(s: string): string { return capitalize(s, "_").replace("_", " "); } -export function camelToSnakeCase(s: string) { +export function camelToSnakeCase(s: string): string { return s.replace(/([A-Z])/g, (m) => "_" + m.toLowerCase()).replace(/^_/, ""); } diff --git a/js/util/zod_util.test.ts b/js/util/zod_util.test.ts index e9c41c7e5..38df59975 100644 --- a/js/util/zod_util.test.ts +++ b/js/util/zod_util.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "vite-plus/test"; import { objectNullish, parseNoStrip } from "./zod_util"; import { z } from "zod/v3"; diff --git a/js/util/zod_util.ts b/js/util/zod_util.ts index 843223ac5..c88206b51 100644 --- a/js/util/zod_util.ts +++ b/js/util/zod_util.ts @@ -34,7 +34,10 @@ export class ExtraFieldsError extends Error { // Note: this check is not exactly equivalent to a deep version of `z.strict()`. // For instance, schemas which intentionally strip keys from objects using // something like `z.transform` can fail this check. -export function parseNoStrip(schema: T, input: unknown) { +export function parseNoStrip( + schema: T, + input: unknown, +): z.TypeOf { const output = schema.parse(input) as z.infer; forEachMissingKey({ lhs: output, diff --git a/js/vite.config.ts b/js/vite.config.ts new file mode 100644 index 000000000..ac822abd1 --- /dev/null +++ b/js/vite.config.ts @@ -0,0 +1,203 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vite-plus"; +import type { PackUserConfig } from "vite-plus/pack"; +import { readFileSync } from "node:fs"; + +const __dirname = import.meta.dirname; + +const pkg = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +); +const define = { __BRAINTRUST_SDK_VERSION__: JSON.stringify(pkg.version) }; +const common = { + sourcemap: true, + // Clean the whole package output, including obsolete runtime directories. + clean: ["dist"], + fixedExtension: false, + define, + dts: { generator: "oxc" as const }, + deps: { + onlyBundle: ["async"], + }, +}; +const entries = { + index: "src/exports.ts", + instrumentation: "src/instrumentation/index.ts", + "custom-views": "src/custom-views/exports.ts", + preprocessors: "src/preprocessors/exports.ts", + auto: "src/auto.ts", +}; + +export default defineConfig({ + // Prefer TypeScript over JavaScript for extension-less imports in sdk/js tests + resolve: { + tsconfigPaths: true, + extensions: [".ts", ".tsx", ".mts", ".js", ".mjs", ".jsx", ".json"], + alias: { + "#braintrust-platform": path.resolve(__dirname, "src/isomorph.ts"), + "@apm-js-collab/code-transformer": path.resolve( + __dirname, + "src/auto-instrumentations/orchestrion-js/index.ts", + ), + }, + }, + server: { + watch: { + ignored: [ + "**/node_modules/**", + "**/dist/**", + "**/vendor/**", + "**/.{idea,git,cache,output,temp}/**", + ], + }, + fs: { + // Deny access to vendor folder + deny: [path.resolve(__dirname, "vendor")], + }, + }, + optimizeDeps: { + exclude: ["vendor/**"], + }, + test: { + reporters: ["default"], + exclude: [ + // Default vitest exclusions + "**/node_modules/**", + "**/dist/**", + "**/cypress/**", + "**/.{idea,git,cache,output,temp}/**", + "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*", + // Exclude vendor folder and all its contents + "**/vendor/**", + "vendor/**", + "./vendor/**", + // Exclude subdirectories with their own test configs + "src/wrappers/ai-sdk/**", + "src/wrappers/claude-agent-sdk/**", + "src/wrappers/vitest/**", + "smoke/**", + // Exclude example tests (require API keys and make real API calls) + "examples/vitest/**", + "examples/node-test/**", + ], + testTimeout: 15_000, + }, + run: { + tasks: { + compile: { + command: "vp pack", + dependsOn: [ + { task: "compile", from: ["dependencies", "devDependencies"] }, + ], + cache: { + input: [{ auto: true }, "!dist/**"], + untrackedEnv: ["SHELL", "BASH_ENV"], + output: ["dist/**"], + }, + }, + }, + }, + pack: [ + ...(["node", "browser", "workerd", "edge-light"] as const).map( + (runtime): PackUserConfig => ({ + ...common, + name: runtime, + // Preserve internal module boundaries so consumers can discard unused + // features through the root export without public feature subpaths. + unbundle: true, + outputOptions: { + // Keep bundled dependencies inside our ESM package scope. A nested + // node_modules directory makes loaders treat them as another package. + sanitizeFileName: (name) => + name + .replaceAll("\0", "_") + .replaceAll("node_modules", "_vendor") + .replaceAll(".pnpm", "_pnpm"), + }, + dts: { + generator: "oxc", + compilerOptions: { + paths: { "#braintrust-platform": [`./src/${runtime}/isomorph.ts`] }, + }, + }, + ...(runtime === "node" + ? {} + : { platform: "browser" as const, target: "es2022" }), + outDir: `dist/${runtime}`, + entry: { + ...entries, + "apply-auto-instrumentation": + runtime === "node" + ? "src/node/apply-auto-instrumentation-entry.ts" + : "src/non-node/apply-auto-instrumentation-entry.ts", + ...(runtime === "node" + ? { + "vitest-evals-reporter": + "src/wrappers/vitest-evals/reporter.ts", + } + : {}), + }, + // Resolve before tsconfig paths: Pack's ordinary alias option loses to + // the source-test fallback and would put Node adapters in browser builds. + plugins: [ + { + name: "braintrust-platform", + resolveId: { + order: "pre", + handler(id) { + if (id === "#braintrust-platform") + return fileURLToPath( + new URL(`./src/${runtime}/isomorph.ts`, import.meta.url), + ); + }, + }, + }, + ], + }), + ), + { + ...common, + name: "tooling", + outDir: "dist/tooling", + entry: Object.fromEntries( + [ + "index", + "bundler/vite", + "bundler/webpack", + "bundler/next", + "bundler/esbuild", + "bundler/rollup", + ].map((name) => [name, `src/auto-instrumentations/${name}.ts`]), + ), + }, + { + ...common, + name: "loader", + format: "cjs", + dts: false, + outDir: "dist/loaders", + entry: { + "webpack-loader": "src/auto-instrumentations/bundler/webpack-loader.ts", + }, + }, + { + ...common, + name: "hooks", + dts: false, + outDir: "dist/hooks", + entry: { + hook: "src/auto-instrumentations/hook.mts", + "loader/esm-hook": "src/auto-instrumentations/loader/esm-hook.mts", + }, + fixedExtension: true, + deps: { neverBundle: ["../auto.js"] }, + outputOptions: { + paths: { + [fileURLToPath(new URL("./src/auto.js", import.meta.url))]: + "../node/auto.js", + }, + }, + }, + ], +}); diff --git a/js/vitest.config.js b/js/vitest.config.js deleted file mode 100644 index 7d680a0a5..000000000 --- a/js/vitest.config.js +++ /dev/null @@ -1,65 +0,0 @@ -import tsconfigPaths from "vite-tsconfig-paths"; -import path from "path"; - -const config = { - plugins: [ - tsconfigPaths({ - // Explicitly specify the root tsconfig to prevent scanning vendor folders - root: ".", - projects: ["./tsconfig.json"], - }), - ], - // Prefer TypeScript over JavaScript for extension-less imports in sdk/js tests - resolve: { - extensions: [".ts", ".tsx", ".mts", ".js", ".mjs", ".jsx", ".json"], - alias: { - "@apm-js-collab/code-transformer": path.resolve( - __dirname, - "src/auto-instrumentations/orchestrion-js/index.ts", - ), - // Prevent resolution into vendor directories - vendor: false, - }, - }, - server: { - fs: { - // Deny access to vendor folder - deny: [path.resolve(__dirname, "vendor")], - }, - }, - optimizeDeps: { - exclude: ["vendor/**"], - }, - test: { - reporters: ["default"], - exclude: [ - // Default vitest exclusions - "**/node_modules/**", - "**/dist/**", - "**/cypress/**", - "**/.{idea,git,cache,output,temp}/**", - "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*", - // Exclude vendor folder and all its contents - "**/vendor/**", - "vendor/**", - "./vendor/**", - // Exclude subdirectories with their own test configs - "src/wrappers/ai-sdk/**", - "src/wrappers/claude-agent-sdk/**", - "src/wrappers/vitest/**", - "smoke/**", - // Exclude example tests (require API keys and make real API calls) - "examples/vitest/**", - "examples/node-test/**", - ], - // Additional test environment configuration - watchExclude: [ - "**/node_modules/**", - "**/dist/**", - "**/vendor/**", - "**/.{idea,git,cache,output,temp}/**", - ], - testTimeout: 15_000, - }, -}; -export default config; diff --git a/knip.jsonc b/knip.jsonc index 202457490..71c09b740 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -19,7 +19,7 @@ "e2e": { "entry": [ "package.json", - "vitest.config.mts", + "vite.config.ts", "helpers/**/*.ts", "scenarios/**/*.test.ts", "scenarios/**/package.json", @@ -46,13 +46,19 @@ "js": { "entry": [ "src/node/index.ts", + "src/exports.ts", + "src/auto.ts", + "src/auto-instrumentations/{index.ts,hook.mts,loader/esm-hook.mts}", + "src/{node,non-node}/apply-auto-instrumentation-entry.ts", + "src/{node,browser,workerd,edge-light}/{isomorph,config}.ts", + "src/{custom-views,preprocessors}/exports.ts", "src/browser/index.ts", "src/edge-light/index.ts", "src/workerd/index.ts", "src/instrumentation/index.ts", "src/auto-instrumentations/bundler/*.ts", "tests/**/*.{ts,tsx,mts,cts,cjs,mjs}", - "scripts/**/*.ts", + "scripts/**/*.{ts,mjs}", "examples/**/*.{ts,tsx,js,jsx,mjs,cjs}", "smoke/**/*.{ts,tsx,js,jsx,mjs,cjs}", "util/**/*.ts", @@ -60,7 +66,7 @@ "project": [ "src/**/*.{ts,tsx,mts,cts}", "tests/**/*.{ts,tsx,mts,cts,cjs,mjs}", - "scripts/**/*.ts", + "scripts/**/*.{ts,mjs}", "examples/**/*.{ts,tsx,js,jsx,mjs,cjs}", "smoke/**/*.{ts,tsx,js,jsx,mjs,cjs,d.ts}", "util/**/*.ts", @@ -74,8 +80,13 @@ "entry": ["*.test.ts"], "project": ["*.ts"], }, + "integrations/deepseek-harness": { + "entry": ["src/index.ts", "src/**/*.test.ts"], + "project": ["src/**/*.ts"], + }, "integrations/otel-js": { "entry": [ + "src/index.ts", "src/**/*.test.ts", "tests/**/*.ts", "examples/**/*.{ts,js,mjs,cjs}", @@ -89,11 +100,15 @@ ], }, "integrations/temporal-js": { - "entry": ["examples/**/*.{ts,js,mjs,cjs}"], + "entry": [ + "src/index.ts", + "src/workflow-interceptors.ts", + "examples/**/*.{ts,js,mjs,cjs}", + ], "project": ["src/**/*.ts", "examples/**/*.{ts,js,mjs,cjs}"], }, "integrations/templates-nunjucks": { - "entry": ["smoke/**/*.{ts,tsx,js,jsx,mjs,cjs}"], + "entry": ["src/index.ts", "smoke/**/*.{ts,tsx,js,jsx,mjs,cjs}"], "project": ["src/**/*.ts", "smoke/**/*.{ts,tsx,js,jsx,mjs,cjs,d.ts}"], }, }, diff --git a/package.json b/package.json index 377e36232..60392354d 100644 --- a/package.json +++ b/package.json @@ -6,27 +6,32 @@ "private": true, "scripts": { "preinstall": "node -e \"const userAgent = process.env.npm_config_user_agent || ''; if (process.env.INIT_CWD === process.cwd() && !userAgent.includes('pnpm/')) { console.error('Use pnpm in this repo.'); process.exit(1); }\"", - "build": "turbo run build", - "check:typings": "turbo run check:typings", - "dev": "turbo run dev", - "watch": "turbo run watch", - "start": "turbo run start", - "clean": "turbo run clean", + "build": "vp run -r build", + "check:typings": "vp run -r check:typings", + "dev": "vp run -r dev", + "watch": "vp run -r watch", + "start": "vp run -r start", + "clean": "vp run -r clean", "knip": "knip --config knip.jsonc --no-config-hints", - "test": "turbo run test --filter=\"!@braintrust/otel\"", - "test:e2e": "turbo run test:e2e", - "test:e2e:update": "turbo run test:e2e:update", + "test": "vp run --filter='*' --filter='@braintrust/*' --filter='!braintrust-sdk-js' --filter='!@braintrust/otel' test", + "test:e2e": "vp run -r test:e2e", + "test:e2e:update": "vp run -r test:e2e:update", "test:e2e:record": "node e2e/scripts/run-record-tests.mjs --braintrust-build-deps", "test:e2e:bump": "node e2e/scripts/run-e2e-bump-docker.mjs", "dotagents:install": "dotagents install", "dotagents:sync": "dotagents sync", "changeset": "changeset", - "playground": "turbo run playground --filter=\"braintrust\"", - "formatting": "prettier --check .", - "lint": "turbo run lint", - "fix:formatting": "prettier --write .", - "fix:lint": "turbo run fix:lint", - "yalc:publish": "turbo run yalc:publish" + "playground": "vp run --filter=braintrust playground", + "formatting": "vp fmt --check .", + "lint": "vp lint .", + "fix:formatting": "vp fmt --write .", + "fix:lint": "vp lint --fix .", + "yalc:publish": "vp run -r yalc:publish", + "pretest:e2e": "pnpm run build", + "pretest:e2e:update": "pnpm run build", + "pretest": "pnpm run build", + "precheck:typings": "pnpm run build", + "check": "vp check" }, "engines": { "pnpm": ">=10.27.0", @@ -41,50 +46,12 @@ "husky": "^9.1.7", "knip": "^5.85.0", "lint-staged": "^16.2.7", - "prettier": "^3.8.1", - "turbo": "^2.9.14", + "vite": "catalog:", + "vite-plus": "catalog:", "yalc": "1.0.0-pre.53" }, "lint-staged": { - "**/*.{js,jsx,ts,tsx,mjs,cjs,css,md,mdx,html,yaml,yml,json}": "prettier --write" + "**/*.{js,jsx,ts,tsx,mjs,cjs,css,md,mdx,html,yaml,yml,json}": "vp fmt --write" }, - "packageManager": "pnpm@10.33.0", - "pnpm": { - "overrides": { - "handlebars": "^4.7.9", - "protobufjs": "^7.6.5", - "body-parser@>=2.0.0 <2.3.0": "^2.3.0", - "brace-expansion@>=3.0.0 <5.0.12": "^5.0.12", - "esbuild": "0.28.1", - "uuid": "11.1.1", - "router@2.2.0>path-to-regexp": "8.4.0", - "express@5.2.1>qs": "6.16.0", - "qs@>=6.7.0 <6.16.0": "^6.16.0", - "tar@>=7.0.0 <7.5.21": "^7.5.21", - "tar@>=6.0.0 <7.0.0": "^6.2.1", - "flatted@<3.4.2": "^3.4.2", - "markdown-it@>=13.0.0 <14.1.1": "^14.1.1", - "@mikro-orm/knex@>=6.0.0 <6.6.14": "^6.6.14", - "glob@>=10.2.0 <10.5.0": "^10.5.0", - "fast-uri@<3.1.8": "^3.1.8", - "linkify-it@<5.0.2": "^5.0.2", - "brace-expansion@<1.1.17": "^1.1.17", - "ip-address@<10.2.2": "^10.2.2", - "brace-expansion@2.1.2": "^2.1.3", - "hono@<4.12.34": "^4.12.34", - "@hono/node-server@<1.19.15": "^1.19.15", - "nanoid@<3.3.18": "^3.3.18", - "postcss@<8.5.23": "^8.5.23", - "js-yaml@3": "^3.15.1", - "js-yaml@4": "^4.3.1", - "@opentelemetry/core@2": "^2.8.0", - "@ai-sdk/provider-utils": "^4.0.33" - }, - "ignoredBuiltDependencies": [ - "@swc/core", - "esbuild", - "msw", - "protobufjs" - ] - } + "packageManager": "pnpm@10.33.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9444e1a5..f4386f6d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,18 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + vite: + specifier: npm:@voidzero-dev/vite-plus-core@1.1.0 + version: 1.1.0 + vite-plus: + specifier: 1.1.0 + version: 1.1.0 + vitest: + specifier: 5.0.3 + version: 5.0.3 + overrides: handlebars: ^4.7.9 protobufjs: ^7.6.5 @@ -33,6 +45,8 @@ overrides: js-yaml@4: ^4.3.1 '@opentelemetry/core@2': ^2.8.0 '@ai-sdk/provider-utils': ^4.0.33 + vite@*: npm:@voidzero-dev/vite-plus-core@1.1.0 + vitest@*: 5.0.3 importers: @@ -59,12 +73,12 @@ importers: lint-staged: specifier: ^16.2.7 version: 16.2.7 - prettier: - specifier: ^3.8.1 - version: 3.8.1 - turbo: - specifier: ^2.9.14 - version: 2.9.14 + vite: + specifier: 'catalog:' + version: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) yalc: specifier: 1.0.0-pre.53 version: 1.0.0-pre.53 @@ -78,12 +92,12 @@ importers: '@types/node': specifier: ^22.10.0 version: 22.19.1 - tsup: - specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) typescript: specifier: ^5.7.2 version: 5.9.3 + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) e2e: devDependencies: @@ -120,9 +134,12 @@ importers: typescript: specifier: 5.4.4 version: 5.4.4 + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0) vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) zod: specifier: 4.3.6 version: 4.3.6 @@ -142,12 +159,12 @@ importers: '@types/node': specifier: ^20.10.5 version: 20.19.40 - tsup: - specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) typescript: specifier: ^5.3.3 version: 5.9.3 + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) integrations/deepseek-harness: dependencies: @@ -176,15 +193,12 @@ importers: '@types/node': specifier: ^22.15.21 version: 22.19.1 - tsup: - specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) typescript: specifier: ^5.5.4 version: 5.9.3 - vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) integrations/otel-js: dependencies: @@ -210,15 +224,12 @@ importers: braintrust: specifier: workspace:* version: link:../../js - tsup: - specifier: ^8.5.0 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) typescript: specifier: 5.5.4 version: 5.5.4 - vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) integrations/templates-nunjucks: dependencies: @@ -235,15 +246,15 @@ importers: braintrust: specifier: workspace:* version: link:../../js - tsup: - specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) typescript: specifier: 5.5.4 version: 5.5.4 + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) integrations/temporal-js: devDependencies: @@ -258,7 +269,7 @@ importers: version: 1.14.1 '@temporalio/worker': specifier: ^1.14.1 - version: 1.14.1(esbuild@0.28.1)(postcss@8.5.26) + version: 1.14.1(esbuild@0.28.1) '@temporalio/workflow': specifier: ^1.14.1 version: 1.14.1 @@ -268,15 +279,15 @@ importers: braintrust: specifier: workspace:* version: link:../../js - tsup: - specifier: ^8.5.0 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) typescript: specifier: 5.5.4 version: 5.5.4 + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) js: dependencies: @@ -301,9 +312,6 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 - esbuild: - specifier: 0.28.1 - version: 0.28.1 esquery: specifier: ^1.7.0 version: 1.7.0 @@ -353,12 +361,6 @@ importers: '@types/react': specifier: ^18.3.1 version: 18.3.31 - '@typescript-eslint/eslint-plugin': - specifier: ^8.49.0 - version: 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4))(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4) - '@typescript-eslint/parser': - specifier: ^8.49.0 - version: 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4) ai: specifier: ^6.0.0 version: 6.0.37(zod@3.25.76) @@ -368,18 +370,12 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 - eslint: - specifier: ^9.39.2 - version: 9.39.4(jiti@2.6.1) - eslint-plugin-es-x: - specifier: 9.7.0 - version: 9.7.0(eslint@9.39.4(jiti@2.6.1)) + esbuild: + specifier: 0.28.1 + version: 0.28.1 eslint-plugin-n: specifier: ^18.2.1 version: 18.2.1(eslint@9.39.4(jiti@2.6.1))(ts-declaration-location@1.0.7(typescript@5.4.4))(typescript@5.4.4) - eslint-plugin-node-import: - specifier: ^1.0.5 - version: 1.0.5(eslint@9.39.4(jiti@2.6.1)) openai: specifier: 6.25.0 version: 6.25.0(ws@8.21.3)(zod@3.25.76) @@ -395,9 +391,6 @@ importers: ts-declaration-location: specifier: ^1.0.7 version: 1.0.7(typescript@5.4.4) - tsup: - specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -411,17 +404,17 @@ importers: specifier: 5.4.4 version: 5.4.4 vite: - specifier: ^8.1.5 - version: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - vite-tsconfig-paths: - specifier: ^4.3.2 - version: 4.3.2(typescript@5.4.4)(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + specifier: 'catalog:' + version: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)' + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0) vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4)) webpack: specifier: ^5.106.2 - version: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26) + version: 5.106.2(esbuild@0.28.1) zod: specifier: ^3.25.34 version: 3.25.76 @@ -434,9 +427,12 @@ importers: typescript: specifier: 5.4.4 version: 5.4.4 + vite-plus: + specifier: 'catalog:' + version: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0) vitest: - specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) packages: @@ -460,14 +456,35 @@ packages: resolution: {integrity: sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg==} engines: {node: '>=18'} + '@andrewbranch/untar.js@1.0.4': + resolution: {integrity: sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==} + '@anthropic-ai/sdk@0.60.0': resolution: {integrity: sha512-9zu/TXaUy8BZhXedDtt1wT3H4LOlpKDO1/ftiFpeR3N1PCr3KJFKkxxlQWWt1NNp08xSwUNJ3JNY8yhl8av6eQ==} hasBin: true + '@arethetypeswrong/core@0.18.5': + resolution: {integrity: sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==} + engines: {node: '>=20'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@blazediff/core@1.10.0': + resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==} + + '@braidai/lang@1.1.2': + resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} + '@changesets/apply-release-plan@7.1.0': resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} @@ -933,10 +950,6 @@ packages: '@types/node': optional: true - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -999,6 +1012,9 @@ packages: peerDependencies: tslib: '2' + '@loaderkit/resolve@1.0.6': + resolution: {integrity: sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1117,8 +1133,12 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/runtime@0.153.0': + resolution: {integrity: sha512-rXJvtlsVG4xJNq64hiaUTIatE7jbBgpa6l4QgmEa2jLgT3ULH9scUBkpsC9N1DfmqixLEyT6DuK6OSXozziIlw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.153.0': + resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -1228,9 +1248,286 @@ packages: cpu: [x64] os: [win32] - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} + '@oxfmt/binding-android-arm-eabi@0.72.0': + resolution: {integrity: sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.72.0': + resolution: {integrity: sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.72.0': + resolution: {integrity: sha512-LjQ8tlevdtwraFlWC6AnQ1Forzde0clRmnsP3QmK+ydSElDt/J+5Jc+m+//Ly1G97aP7WCNUw110QMIUw2K7xA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.72.0': + resolution: {integrity: sha512-+ZbcxhB1C34RQR5IldSyY6Z/yalJIxHXhP36PGE4sEq0f/lgL5jCLmOKoDeVm8dqd9gFGDsd1bNWHSGCAwdSkw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.72.0': + resolution: {integrity: sha512-yGPhA8sePEBFV9iGcprhyOk3Bz3fZshswMH3DpYIPZFLT1DG5LjiZ0j6AJQpSueaL6bUK0av1K+5+fe+ldy5EQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.72.0': + resolution: {integrity: sha512-/g5axuqFMcAtoQrFLR5EJEMDjR5Ea4GuzpdDmMpPWAgrF1DmL4kD1NPNVlpXEOxpFJnD6lPvLKYj1lzdlrfifA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.72.0': + resolution: {integrity: sha512-InVitdHbS09PM+w+WinIiyX37rC9R3L7lUP9mKy9uKTEDSUFaLjWsLoreRIxKivnv7Ndqc6Trvi+K+b6ZzhiJg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.72.0': + resolution: {integrity: sha512-WKjlyRAxpPjXoqeuwFBim5LscgvY+RNEiZ971C9JlRQZONhJTsXDki/oHFYQE+fzzRpzXh3owZvOdT3kNL2g+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.72.0': + resolution: {integrity: sha512-poafnbsPpJhR+5Mbwpf8CscugTyiRAzjTOxRVn8ra3X2jnURs2xL5Du2xl5g2iW85lICMglpmLf8jDjZEbNgzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.72.0': + resolution: {integrity: sha512-zNUi0gKRsO7S3IfPzsTbnm88OedPuX32hmZFWZRWHvWJlAoZRwVbeO6Nb3hng4vDgL7qUKp4TGqzPz1mhsycYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.72.0': + resolution: {integrity: sha512-qTY7z/iyF/rWI2HWwWMevXqZW5PZUOofUy2afK7v6rseXnpld3yjgyIk6pCux3DqWQ0lB+f5atvLJNBwzLvFsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.72.0': + resolution: {integrity: sha512-w18qfo8vEMD6EKEHtzTvjc6jH8yCyXIT1Bn7PFWJd0PpZyzH/VQDx4xvjCh0LGz6GRzqiezbisTdG9AoXtpkZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.72.0': + resolution: {integrity: sha512-grW8HwEir+vK7fCcq1ZgL3NHQCfZ1PidiERE9Hbf+24xGys+DoDaoqTht4ccfn0PcBwbViLkwkda/KNpJu2KRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.72.0': + resolution: {integrity: sha512-wQJRQfWBRIn88Pkr449EUViwRayrYUScg7ty+Ihiem21mQ9BUA6S9e/4RAgyNk9lMag0KBouyKskjq61rOC0WQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.72.0': + resolution: {integrity: sha512-T1B0uQzafxuGPjjaPrZJ497nxoi1BiI65JfsYc56nNpmev3msfb/iaTmH2kQuNFqa8PKxpMlf3GhWrWBvBPORQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.72.0': + resolution: {integrity: sha512-OKxxgTurl7+hypVplO8QRAbbQlwEs0j1Y6FZv+ZN2qNbdxQk2muMlzuJNBGTCWwxQ0rkkOD1uK/DW1fvzoUSJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.72.0': + resolution: {integrity: sha512-L78TKzURqvjuZOCo88ixMbPXf/RP7u31Y0cmjJ/KEmrHmfYhmhEvsDaTYPiyTRne64jUgCbADfY0YeoQk4/Xyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.72.0': + resolution: {integrity: sha512-gQtFW+Ii8BWgtJTAD17TY/WsdNS5gUfAPov8iI1jeq4r9lRRYImmxPL08UU00/RLaCfBJwh2UgINnDY0iOiZ+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.72.0': + resolution: {integrity: sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@7.0.2003': + resolution: {integrity: sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@7.0.2003': + resolution: {integrity: sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@7.0.2003': + resolution: {integrity: sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@7.0.2003': + resolution: {integrity: sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@7.0.2003': + resolution: {integrity: sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@7.0.2003': + resolution: {integrity: sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.87.0': + resolution: {integrity: sha512-22tVIHlB9898OYsBKmYB6He1MCSqdTmK1BVWuSNUTVrpS0KWNege+U9uEYqGJ+KatQag0r/oU44i3OOs46z7HQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.87.0': + resolution: {integrity: sha512-JfZQ3MWf74n8ov0DojZpkOduIubYJvZL5tbTMMhDutsal6u1xH1QuNpN+lK+EQwF8AeAfgwTR2fgh5U2br27XA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.87.0': + resolution: {integrity: sha512-rkx6Xvvw93z2GImVOyvbZwAZcoY0Z6MJW177XNsSB+i/CBsH7ANyelTp01VZyIkM3iSXtqKVb4+ptBgPHap5pQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.87.0': + resolution: {integrity: sha512-cw5nYw7L/pQ2SgGBgOcg/Qo89llQre3vIc/kuBLhSICzu1r5s8L76H3ug6AIFsD98WpROj8/z5JVzkf4daxHaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.87.0': + resolution: {integrity: sha512-x/SuznE6loRZK9gz9AS4ZmzNzSu+5KdeoCS+sVHhnrE8HM6AD8UyTjwldQTTqlXGvkwHASgB0NiTNOc3o3PdAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.87.0': + resolution: {integrity: sha512-+JEDmCAXiiFmKhAjZhgAOwaHWY6WENOQ5QA+lFuY59tgsUCDOTRi46B+lENK6CWvtxsXpwkZXNOgpjAQcpIpnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.87.0': + resolution: {integrity: sha512-/uC9XqXLAC9AHFLNycwmSQEvKqGrOCgU+uXALI7gNW0DG81TJ/3T2eHnKdRvcb5R1gFEfjxDwI4e4zIa29rICQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.87.0': + resolution: {integrity: sha512-FTRYPsBy/M728vnUv7l+KAwSAa3Qi4BQbhVvy7NYuMTZ0sUa/OWsbMx79HVFw6u/5KWcaYayDsoOiv7xbevzDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.87.0': + resolution: {integrity: sha512-6sC+N7IY0h1egd8SrYUGNN7CUyKJZ4bIZakKa7V7hfinf4jfi8Kq4+klJvFp5DqXcwMn44I5MeBwmJluO1odFg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.87.0': + resolution: {integrity: sha512-d5Mnn+ICbhoJPi37Ccjy+86T5qfH2+mSTrTMBXNPfB+wqSWOayBBLLIrCXdUy1YssluCVdGkl+DO9w5r5ODmUA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.87.0': + resolution: {integrity: sha512-sf5YWuUo1Dx9loqWCgF8P278MycfcD4JHjDC/LV5j11L3Lqa4PFiwCjD6B8kP4AjsgZZkLn3xbpJEtZBaccs4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.87.0': + resolution: {integrity: sha512-+aCraYbmK3uU3P0rFojjeaK9pGY6JvBDXh/0CM+gPyWKmNmXmnFe0qj4qj02U4ibFpbd9Pa9CmDvIdjJUd29rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.87.0': + resolution: {integrity: sha512-7rzbZQIljo6VLWVPqUyLa2P/WV3VFsJB4M52R4NCUJn1A50yjtUsHxiMHFL9opEKGFIiTbZ3VQzmtcJOPUq4NQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.87.0': + resolution: {integrity: sha512-Dry46Fa+AXE/xt9J/YriujU1pU9mzfNWHGQwtOYibAFH8HLrKWCi93AhIGnuUMiwn2JbuBkRyz51f50Ly4r4ow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.87.0': + resolution: {integrity: sha512-f/HMZez74gJvnRpGz+b5sdGyWrEgRLmFTXg9sNoqjPWogGxYHUhXKTKyMiYsu9rUGR2ciYqL3fk+Ua4JhMJbRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.87.0': + resolution: {integrity: sha512-lEC6ZfPZm/07JlB7FsOWX9o1RiHlXiBW594LSUGuXmekC8LZmvUPP9XzE2WZcNxJi5cHA/Fw56o02c3JxgMA5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.87.0': + resolution: {integrity: sha512-afBWKOD36dUiA9AenaiWpCg8xfyUVPYznyAAVXu413brK4RA5UI/Xoc4yfeGIT8G1a44MV+JR7hioryerBDzGQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.87.0': + resolution: {integrity: sha512-daptNsEOuru4/nJTxV4DBHNJ5GepzmaBVQf+jjgKQGi9NRlZKik+RFq4hUL7ewsF1pwjKM+SkYaMbF82UwFGnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.87.0': + resolution: {integrity: sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/plugins@1.79.0': + resolution: {integrity: sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1259,97 +1556,102 @@ packages: '@protobufjs/utf8@1.1.1': resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + '@publint/pack@0.1.7': + resolution: {integrity: sha512-4EDEmvxWtgsCnnVeBvtFIFZtUhPPt1+bA9JrSwU4Sa//6oKtzCSlGGXYJr44OD9aGISymbieJ4mCKHUygUDU+g==} + engines: {node: '>=18'} + + '@rolldown/binding-android-arm-eabi@1.2.13': + resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.13': + resolution: {integrity: sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + '@rolldown/binding-darwin-arm64@1.2.13': + resolution: {integrity: sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + '@rolldown/binding-darwin-x64@1.2.13': + resolution: {integrity: sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + '@rolldown/binding-freebsd-x64@1.2.13': + resolution: {integrity: sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.13': + resolution: {integrity: sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + '@rolldown/binding-linux-arm64-gnu@1.2.13': + resolution: {integrity: sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + '@rolldown/binding-linux-arm64-musl@1.2.13': + resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.13': + resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + '@rolldown/binding-linux-s390x-gnu@1.2.13': + resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + '@rolldown/binding-linux-x64-gnu@1.2.13': + resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + '@rolldown/binding-linux-x64-musl@1.2.13': + resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + '@rolldown/binding-openharmony-arm64@1.2.13': + resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + '@rolldown/binding-win32-arm64-msvc@1.2.13': + resolution: {integrity: sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + '@rolldown/binding-win32-x64-msvc@1.2.13': + resolution: {integrity: sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1618,39 +1920,22 @@ packages: resolution: {integrity: sha512-MzshcoRo8zjQYa9WHrv3XC8LVvpRNSVaW3kOSTmHuTYDh/7be48WODOgs5yUpbnkpsw6rjVCDCgtB/K02cQwDg==} engines: {node: '>= 18.0.0'} - '@turbo/darwin-64@2.9.14': - resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} - cpu: [x64] - os: [darwin] - - '@turbo/darwin-arm64@2.9.14': - resolution: {integrity: sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==} - cpu: [arm64] - os: [darwin] - - '@turbo/linux-64@2.9.14': - resolution: {integrity: sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==} - cpu: [x64] - os: [linux] - - '@turbo/linux-arm64@2.9.14': - resolution: {integrity: sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==} - cpu: [arm64] - os: [linux] - - '@turbo/windows-64@2.9.14': - resolution: {integrity: sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==} - cpu: [x64] - os: [win32] + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} - '@turbo/windows-arm64@2.9.14': - resolution: {integrity: sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==} - cpu: [arm64] - os: [win32] + '@testing-library/user-event@14.6.7': + resolution: {integrity: sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/async@3.2.24': resolution: {integrity: sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==} @@ -1708,101 +1993,172 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - '@typescript-eslint/eslint-plugin@8.50.0': - resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.50.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@vercel/functions@1.0.2': + resolution: {integrity: sha512-j3udyHOv/05Y8o3WQ/ANMWa1aYagsY5B3ouImiwgYsz5z4CBUHTY5dk74oQAXYr+bgoVDpdDlmxkpnxGzKEdLQ==} + engines: {node: '>= 16'} - '@typescript-eslint/parser@8.50.0': - resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} - '@typescript-eslint/project-service@8.50.0': - resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/browser-preview@5.0.3': + resolution: {integrity: sha512-jfnEBv5yDRPR1MeiTlTW5bcZoPsiI7MBhpZR8IJiSxVhdK6Pa4gPk3tLHTRxVHy24kkWxY2TnUbnszU3J1bQJw==} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + vitest: 5.0.3 - '@typescript-eslint/scope-manager@8.50.0': - resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.50.0': - resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/browser@5.0.3': + resolution: {integrity: sha512-7KHIkVA/gTRxQV9qPa9qLK+g5INZG6iRrlwXSFG5EKsOomW3UzzW7wsdt1Yp5PufzQ43lniuONlb4lUVCC2qSg==} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + vitest: 5.0.3 - '@typescript-eslint/type-utils@8.50.0': - resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/types@8.50.0': - resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.50.0': - resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true - '@typescript-eslint/utils@8.50.0': - resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@vitest/pretty-format@5.0.3': + resolution: {integrity: sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==} - '@typescript-eslint/visitor-keys@8.50.0': - resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/snapshot@5.0.3': + resolution: {integrity: sha512-SK4YTg9jL+jwakHIZxfciuvyOQsbY15AueR3USGtsXzmy+3FbJgqjAbC+ac20mRLwfEDtO5sfTqK6bUCDTRVPA==} - '@vercel/functions@1.0.2': - resolution: {integrity: sha512-j3udyHOv/05Y8o3WQ/ANMWa1aYagsY5B3ouImiwgYsz5z4CBUHTY5dk74oQAXYr+bgoVDpdDlmxkpnxGzKEdLQ==} - engines: {node: '>= 16'} + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} - '@vercel/oidc@3.1.0': - resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} - engines: {node: '>= 20'} + '@vitest/ui@5.0.3': + resolution: {integrity: sha512-9dfvqDBvSbHM6y9YVXxAypkl2z7mrWozK/JFo237qI0cj/3M/kzDKiVZqzi3TE54Ler7Jditud7OMBZCyUvRHA==} + peerDependencies: + vitest: 5.0.3 - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + '@vitest/utils@5.0.3': + resolution: {integrity: sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==} - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@voidzero-dev/vite-plus-core@1.1.0': + resolution: {integrity: sha512-Ai6HQP1zG0H3ZrmStEaMVKtDzW0xU3VgMzm3LFaXDQMEp8nAqvp0VGoU8HmpB9xv9vDSl+UxiSNOpuY9T714OQ==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@arethetypeswrong/core': ^0.18.1 + '@types/node': ^20.19.0 || >=22.12.0 + '@typescript/native-preview': '*' + '@vitejs/devtools': ^0.7.1 + '@volar/typescript': ~2.4.0 + '@vue/language-core': ~3.2.0 || ~3.3.0 + esbuild: 0.28.1 + jiti: '>=1.21.0' + less: ^4.0.0 + publint: ^0.3.8 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: '>=0.5.0' + unrun: '*' + vue-tsc: ~3.2.0 || ~3.3.0 + yaml: ^2.4.2 peerDependenciesMeta: - msw: + '@arethetypeswrong/core': optional: true - vite: + '@types/node': + optional: true + '@typescript/native-preview': + optional: true + '@vitejs/devtools': + optional: true + '@volar/typescript': + optional: true + '@vue/language-core': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + typescript: optional: true + unplugin-unused: + optional: true + unrun: + optional: true + vue-tsc: + optional: true + yaml: + optional: true + + '@voidzero-dev/vite-plus-darwin-arm64@1.1.0': + resolution: {integrity: sha512-iWJdQZBRZWB6X1Aadk0Rrs/NJeP32Qb0OGeWnu1AYL0g6ZCj1yy3l9Oa+HasDyG97s+4UYo523LdBAAv3dlFwg==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [darwin] + + '@voidzero-dev/vite-plus-darwin-x64@1.1.0': + resolution: {integrity: sha512-IYSQ2KdigRMuVkKCC+e5uNliCTSHa8j3ajHsFG2cfXoiGHS1f4ausbMEzNYtn9Tcn0Xsyo7l0LN8lvB7MapJhA==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] + + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-PmrJG8Md5KCWi/akWJz5GRjei2QfRf48R4rPB9CfualzAAlpMOmaMGeV3eloWVAFye85uhdtzCmIAqN5OUVBJA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + '@voidzero-dev/vite-plus-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-zFr57++m5U474XEMcFGV025TOv9V2I5d87yylcuI/SvxQwDE9P7T5iRl55vGzQOzzInSnQuru6ByNIfmGa+84A==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + '@voidzero-dev/vite-plus-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-DXeRuTMylEbIVCQX+pSBM27lSPQwAKO6PDnGBJ6nFGhu37AmNeVl7cIChtcniXOuykbFduhSt3eIr/6CZHoEag==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + '@voidzero-dev/vite-plus-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-J7/Mb/kxtENUsL5Gh+8SSr/p054fSqU4oj0GEthr/2VEFSwZouOUigk6CzE7ANKFeTNJm40n8QCnjm7Oiy09pQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-EXWz8Tt7skmI0fLMSq5zYcuHo6SCF0reG55qDB/NMvWVIAca6Dborq7D1HyKA8v6p39zOl8r07gR7yXuYv7eeQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [win32] - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@voidzero-dev/vite-plus-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-K9k77Mj/TszFHKsZePKsyfsIPo8HbjKeJUxaSJ6YZKAmSh9QvDsMFe4i4/HALwyii/PR1EQ8N0iN1nf9cNimGQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1855,6 +2211,165 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@yuku-codegen/binding-android-arm64@0.9.5': + resolution: {integrity: sha512-jrOY5WM+AaAqkv51fHP1x28ifto4WgcZVzodLUyMU1jMWn5Sq+VciRdk/n8E0Ey5w4p4cWWE+m5GfXWOYh7Kzw==} + cpu: [arm64] + os: [android] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-darwin-arm64@0.9.5': + resolution: {integrity: sha512-4O4lkCQIzPZGjiNB1GORSI6hBECbiiSBS/APZXPvNKYH+nhY4uuqv03LNXA+SET3hoBjvr95P5rIhY8KQaQUBA==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-darwin-x64@0.9.5': + resolution: {integrity: sha512-9EvoUO0SEhD6/d8VHdWuPerepPMSR1y84+UEgz8Un1Ope14Oe7xMvePpEQLTLovoIFZ8Zg3iZ8z8E11pZMqC3g==} + cpu: [x64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-freebsd-x64@0.9.5': + resolution: {integrity: sha512-HqT78WwgHTmp8lwujoUa9CrIortX4DdpuiVC18ZPSGvuuJf4ylpIEI6QrQEM78Zwz3muhAWAOXZ5irdiYY+AyA==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': + resolution: {integrity: sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm-musl@0.9.5': + resolution: {integrity: sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-win32-arm64@0.9.5': + resolution: {integrity: sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w==} + cpu: [arm64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-win32-x64@0.9.5': + resolution: {integrity: sha512-FxENahEjWSan59Syh/us/Kf4wNq8qrJLFJ3R2N4Oiwtb6yNdz/rWLNTIoNSssnsp7IoWJdUP6o/Z8ppg7lXMcg==} + cpu: [x64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-parser/binding-android-arm64@0.9.5': + resolution: {integrity: sha512-A2JCFCSHfnficqYEw4Iujpx7XrkMM3UfFcgJFmYpZSo9zM4nyhmdIIE0FogSduuU60lhM0/UcfuUBXIVNBMlGQ==} + cpu: [arm64] + os: [android] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-darwin-arm64@0.9.5': + resolution: {integrity: sha512-3PiyU+Eare4YuKaQ22N98/yAiROPY5o/NQJHraICzDvk4pgS+m+bgLKOvkGBRn33OnV95Vdv1Mn38b+MQHpULQ==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-darwin-x64@0.9.5': + resolution: {integrity: sha512-blFMAFI7AInI83XaiOF8cIeiRM46Nz9EfpZtZPRLbKxSA9aAr5v8aHYpmfN6NoyXxAv/10pwS4gsAuc1W6fy5Q==} + cpu: [x64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-freebsd-x64@0.9.5': + resolution: {integrity: sha512-TsNuL4qsZdO0tHp5GW43Y5fHeLPpPHA675wdcPNTcdq2ZO5AvsQWFFoiDg8CH4oBktfsQ9tdvKhjLnLYwKvp4g==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-gnu@0.9.5': + resolution: {integrity: sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-musl@0.9.5': + resolution: {integrity: sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-win32-arm64@0.9.5': + resolution: {integrity: sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg==} + cpu: [arm64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-win32-x64@0.9.5': + resolution: {integrity: sha512-FrERt9YWatY3bJfSUdi4YWY+6iQcyo3MzCC821BxlWM5TFZEHijnpz/bknR79VHlXY/9CvXz8ZlaAGPsTtN3nw==} + cpu: [x64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-toolchain/types@0.9.5': + resolution: {integrity: sha512-KiuLNNgX9uNealaWAR+G3/cMXnRk9x4TY2EkYe/KIag+UPdwiA0RRf1hr1WAxzTP8KGzCTkqUdLPvqh32sEO3w==} + a-sync-waterfall@1.0.1: resolution: {integrity: sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==} @@ -1934,19 +2449,23 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -1995,16 +2514,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: 0.28.1 - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -2027,10 +2536,6 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -2039,6 +2544,9 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} @@ -2078,10 +2586,6 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - commander@5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} @@ -2089,13 +2593,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - consola@3.4.0: - resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} - engines: {node: ^14.18.0 || >=16.10.0} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2130,6 +2627,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2142,6 +2643,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -2150,9 +2654,6 @@ packages: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.422: resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==} @@ -2162,9 +2663,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} @@ -2208,12 +2706,6 @@ packages: peerDependencies: eslint: '>=8' - eslint-plugin-es-x@9.7.0: - resolution: {integrity: sha512-BT0pUw0toxtrHulYycGiUTL++qC11opyPYMuGUR7AgmuQ/N1YN1xLupF8rGxlaGqEqkqeedJuacCxxAcYgGkvg==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - eslint: '>=9.29.0' - eslint-plugin-n@18.2.1: resolution: {integrity: sha512-aO3C9//yq8JIvYOi/T+jPvcZ9hZzpwzbR8esrYpFtgE9vpbyM8kn42AQOtIqYspVmpaSWr8X+nrlQuAJYxXAaw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2227,12 +2719,6 @@ packages: typescript: optional: true - eslint-plugin-node-import@1.0.5: - resolution: {integrity: sha512-razzgbr3EcB5+bm8/gqTqzTJ7Bpiu8PIChiAMRfZCNigr9GZBtnVSI+wPw+RGbWYCCIzWAsK/A7ihoAeSz5j7A==} - engines: {node: ^14.18.0 || ^16.0.0 || >= 18.0.0} - peerDependencies: - eslint: '>=7' - eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -2241,12 +2727,6 @@ packages: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-type-tracer@0.5.2: - resolution: {integrity: sha512-YiJmgk6OD1suPB4UC3M/PB+oxahf145jQQ5gFXFk9F9Y7puWwnqGYC2BtTB+FoHqdOohImeTt0rNk0lhhECNaQ==} - engines: {node: ^18 || >=20} - peerDependencies: - eslint: '>=8' - eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2364,6 +2844,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2380,19 +2863,12 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} formatly@0.3.0: resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} @@ -2446,11 +2922,6 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -2521,10 +2992,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2578,9 +3045,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -2589,9 +3053,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} js-yaml@3.15.2: resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} @@ -2637,87 +3100,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [musl] - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [glibc] - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [musl] - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@16.2.7: resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} engines: {node: '>=20.17'} @@ -2727,10 +3183,6 @@ packages: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -2759,14 +3211,19 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} marked@4.3.0: resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} @@ -2817,9 +3274,6 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} @@ -2827,6 +3281,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2852,15 +3310,12 @@ packages: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nano-spawn@2.0.0: resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} engines: {node: '>=20.17'} - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2908,10 +3363,6 @@ packages: chokidar: optional: true - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - obug@2.2.1: resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} @@ -2948,6 +3399,36 @@ packages: oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + oxfmt@0.72.0: + resolution: {integrity: sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@7.0.2003: + resolution: {integrity: sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==} + hasBin: true + + oxlint@1.87.0: + resolution: {integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -2976,12 +3457,12 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + package-manager-detector@1.9.0: + resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2998,10 +3479,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3032,33 +3509,12 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: ^8.5.23 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + postcss@8.5.29: + resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -3070,10 +3526,9 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} - engines: {node: '>=14'} - hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} proto3-json-serializer@2.0.2: resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} @@ -3083,6 +3538,11 @@ packages: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} + publint@0.3.25: + resolution: {integrity: sha512-WMMbn/d7JqRlfXQL/JUI4n5skC8cJ5WVr8/wT5mmiYWkUdENkZr1I5rgs2+dQPVRgoJd+JRYtB46MTsuDylw9g==} + engines: {node: '>=18'} + hasBin: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3093,14 +3553,13 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3134,8 +3593,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + rolldown@1.2.13: + resolution: {integrity: sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3150,6 +3609,10 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3179,13 +3642,14 @@ packages: shiki@0.14.7: resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3201,8 +3665,8 @@ packages: resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==} engines: {node: '>= 18'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} source-map-loader@4.0.2: @@ -3228,9 +3692,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -3249,10 +3710,6 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -3281,11 +3738,6 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3364,28 +3816,19 @@ packages: engines: {node: '>=10'} hasBin: true - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - thingies@2.5.0: resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} engines: {node: '>=10.18'} peerDependencies: tslib: ^2 - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinybench@4.0.1: resolution: {integrity: sha512-Nb1srn7dvzkVx0J5h1vq8f48e3TIcbrS7e/UfAI/cDSef/n8yLh4zsAEsFkfpw6auTY+ZaspEvam/xs8nMnotQ==} engines: {node: '>=18.0.0'} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} tinyexec@1.3.1: resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} @@ -3395,6 +3838,10 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@2.2.0: + resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.1: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} @@ -3410,6 +3857,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -3423,66 +3874,19 @@ packages: peerDependencies: tslib: '2' - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-declaration-location@1.0.7: resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} peerDependencies: typescript: '>=4.0.0' - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - tsconfck@3.1.1: - resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==} - engines: {node: ^18 || >=20} - deprecated: unmaintained - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.5.23 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} hasBin: true - turbo@2.9.14: - resolution: {integrity: sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==} - hasBin: true - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3513,14 +3917,16 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.6.1-rc: + resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -3563,21 +3969,33 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true - vite-tsconfig-paths@4.3.2: - resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + vite-plus@1.1.0: + resolution: {integrity: sha512-khkRlY70qqbBV0QmF9QKXSrhxIRYftb1H05AJMowMoq7iouchmAoXYeReZ8pKKMOadqrWVxFa3S9BA2m7w59iA==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + hasBin: true peerDependencies: - vite: '*' + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + playwright: '*' peerDependenciesMeta: - vite: + '@vitest/browser-playwright': + optional: true + '@vitest/browser-webdriverio': + optional: true + playwright: optional: true - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + vite@8.3.4: + resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.7.1 esbuild: 0.28.1 jiti: '>=1.21.0' less: ^4.0.0 @@ -3614,23 +4032,23 @@ packages: yaml: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -3697,9 +4115,9 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} hasBin: true word-wrap@1.2.5: @@ -3713,10 +4131,6 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -3773,6 +4187,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yuku-ast@0.9.5: + resolution: {integrity: sha512-Q8qW8WwQnN5Cm0ZZivdRIfv0sRLTjUq0YumXJkw8CYN1aCdICH9rk4C47/4n6kA5EqDtlj+F608twoTSV2MwdQ==} + + yuku-codegen@0.9.5: + resolution: {integrity: sha512-zGUVyDpSK4b6c9B0yyxi2P0wujn1XZTbG9dCb7d6gyAoP63EMgyLzUwPUvwxPG5jgYqhlI7w+S3oCyapqGr4PA==} + + yuku-parser@0.9.5: + resolution: {integrity: sha512-IBnAdNVMswWbJcM7woPluOudectJzFjJ1N8jVRxP9Uq4CIv5hZdBIcdmtRbFDYF/Ph2j2gVup4YJ4N9mh0jYFA==} + zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -3809,10 +4232,38 @@ snapshots: dependencies: json-schema: 0.4.0 + '@andrewbranch/untar.js@1.0.4': + optional: true + '@anthropic-ai/sdk@0.60.0': {} + '@arethetypeswrong/core@0.18.5': + dependencies: + '@andrewbranch/untar.js': 1.0.4 + '@loaderkit/resolve': 1.0.6 + cjs-module-lexer: 1.4.3 + fflate: 0.8.3 + lru-cache: 11.5.3 + semver: 7.8.0 + typescript: 5.6.1-rc + validate-npm-package-name: 5.0.1 + optional: true + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/runtime@7.28.4': {} + '@blazediff/core@1.10.0': {} + + '@braidai/lang@1.1.2': + optional: true + '@changesets/apply-release-plan@7.1.0': dependencies: '@changesets/config': 3.1.3 @@ -4271,6 +4722,14 @@ snapshots: '@types/node': 22.19.1 optional: true + '@inquirer/confirm@6.0.12(@types/node@24.12.4)': + dependencies: + '@inquirer/core': 11.1.9(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) + optionalDependencies: + '@types/node': 24.12.4 + optional: true + '@inquirer/core@11.1.9(@types/node@20.19.40)': dependencies: '@inquirer/ansi': 2.0.5 @@ -4297,6 +4756,19 @@ snapshots: '@types/node': 22.19.1 optional: true + '@inquirer/core@11.1.9(@types/node@24.12.4)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@24.12.4) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 24.12.4 + optional: true + '@inquirer/external-editor@1.0.3(@types/node@24.12.4)': dependencies: chardet: 2.1.1 @@ -4317,14 +4789,10 @@ snapshots: '@types/node': 22.19.1 optional: true - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + '@inquirer/type@4.0.5(@types/node@24.12.4)': + optionalDependencies: + '@types/node': 24.12.4 + optional: true '@isaacs/fs-minipass@4.0.1': dependencies: @@ -4392,6 +4860,11 @@ snapshots: '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) tslib: 2.8.1 + '@loaderkit/resolve@1.0.6': + dependencies: + '@braidai/lang': 1.1.2 + optional: true + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -4534,7 +5007,9 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} - '@oxc-project/types@0.139.0': {} + '@oxc-project/runtime@0.153.0': {} + + '@oxc-project/types@0.153.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -4601,9 +5076,142 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@pkgjs/parseargs@0.11.0': + '@oxfmt/binding-android-arm-eabi@0.72.0': + optional: true + + '@oxfmt/binding-android-arm64@0.72.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.72.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.72.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.72.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.72.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.72.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.72.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.72.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.72.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.72.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.72.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.72.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.72.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.72.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@7.0.2003': + optional: true + + '@oxlint-tsgolint/darwin-x64@7.0.2003': + optional: true + + '@oxlint-tsgolint/linux-arm64@7.0.2003': + optional: true + + '@oxlint-tsgolint/linux-x64@7.0.2003': + optional: true + + '@oxlint-tsgolint/win32-arm64@7.0.2003': + optional: true + + '@oxlint-tsgolint/win32-x64@7.0.2003': + optional: true + + '@oxlint/binding-android-arm-eabi@1.87.0': + optional: true + + '@oxlint/binding-android-arm64@1.87.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.87.0': + optional: true + + '@oxlint/binding-darwin-x64@1.87.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.87.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.87.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.87.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.87.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.87.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.87.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.87.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.87.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.87.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.87.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.87.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.87.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.87.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.87.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.87.0': optional: true + '@oxlint/plugins@1.79.0': {} + + '@polka/url@1.0.0-next.29': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -4624,53 +5232,54 @@ snapshots: '@protobufjs/utf8@1.1.1': {} - '@rolldown/binding-android-arm64@1.1.5': + '@publint/pack@0.1.7': + dependencies: + tinyexec: 1.3.1 optional: true - '@rolldown/binding-darwin-arm64@1.1.5': + '@rolldown/binding-android-arm-eabi@1.2.13': optional: true - '@rolldown/binding-darwin-x64@1.1.5': + '@rolldown/binding-android-arm64@1.2.13': optional: true - '@rolldown/binding-freebsd-x64@1.1.5': + '@rolldown/binding-darwin-arm64@1.2.13': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + '@rolldown/binding-darwin-x64@1.2.13': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': + '@rolldown/binding-freebsd-x64@1.2.13': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.13': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': + '@rolldown/binding-linux-arm64-gnu@1.2.13': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': + '@rolldown/binding-linux-arm64-musl@1.2.13': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.13': optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': + '@rolldown/binding-linux-s390x-gnu@1.2.13': optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': + '@rolldown/binding-linux-x64-gnu@1.2.13': optional: true - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-linux-x64-musl@1.2.13': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.13': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': + '@rolldown/binding-win32-arm64-msvc@1.2.13': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.5': + '@rolldown/binding-win32-x64-msvc@1.2.13': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -4857,7 +5466,7 @@ snapshots: long: 5.3.2 protobufjs: 7.6.5 - '@temporalio/worker@1.14.1(esbuild@0.28.1)(postcss@8.5.26)': + '@temporalio/worker@1.14.1(esbuild@0.28.1)': dependencies: '@grpc/grpc-js': 1.14.4 '@swc/core': 1.15.8 @@ -4876,11 +5485,11 @@ snapshots: protobufjs: 7.6.5 rxjs: 7.8.2 source-map: 0.7.6 - source-map-loader: 4.0.2(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26)) + source-map-loader: 4.0.2(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)) supports-color: 8.1.1 - swc-loader: 0.2.6(@swc/core@1.15.8)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26)) + swc-loader: 0.2.6(@swc/core@1.15.8)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)) unionfs: 4.6.0 - webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26) + webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1) transitivePeerDependencies: - '@minify-html/node' - '@swc/css' @@ -4902,29 +5511,28 @@ snapshots: '@temporalio/proto': 1.14.1 nexus-rpc: 0.0.1 - '@turbo/darwin-64@2.9.14': - optional: true - - '@turbo/darwin-arm64@2.9.14': - optional: true - - '@turbo/linux-64@2.9.14': - optional: true - - '@turbo/linux-arm64@2.9.14': - optional: true - - '@turbo/windows-64@2.9.14': - optional: true + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.28.4 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 - '@turbo/windows-arm64@2.9.14': - optional: true + '@testing-library/user-event@14.6.7(@testing-library/dom@10.4.2)': + dependencies: + '@testing-library/dom': 10.4.2 '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + '@types/async@3.2.24': {} '@types/chai@5.2.3': @@ -4987,170 +5595,667 @@ snapshots: '@types/statuses@2.0.6': optional: true - '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4))(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4)': + '@vercel/functions@1.0.2': {} + + '@vercel/oidc@3.1.0': {} + + '@vitest/browser-preview@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vitest@5.0.3)': dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4) - '@typescript-eslint/visitor-keys': 8.50.0 - eslint: 9.39.4(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.4.4) - typescript: 5.4.4 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - - supports-color + - bufferutil + - msw + - utf-8-validate + - vite - '@typescript-eslint/parser@8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4)': + '@vitest/browser-preview@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vitest@5.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.4.4) - '@typescript-eslint/visitor-keys': 8.50.0 - debug: 4.4.3 - eslint: 9.39.4(jiti@2.6.1) - typescript: 5.4.4 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - - supports-color + - bufferutil + - msw + - utf-8-validate + - vite - '@typescript-eslint/project-service@8.50.0(typescript@5.4.4)': + '@vitest/browser-preview@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vitest@5.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.4.4) - '@typescript-eslint/types': 8.50.0 - debug: 4.4.3 - typescript: 5.4.4 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - - supports-color + - bufferutil + - msw + - utf-8-validate + - vite - '@typescript-eslint/scope-manager@8.50.0': + '@vitest/browser-preview@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vitest@5.0.3)': dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite - '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.4.4)': + '@vitest/browser-preview@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(vitest@5.0.3)': dependencies: - typescript: 5.4.4 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite - '@typescript-eslint/type-utils@8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4)': + '@vitest/browser-preview@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.4.4) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4) - debug: 4.4.3 - eslint: 9.39.4(jiti@2.6.1) - ts-api-utils: 2.1.0(typescript@5.4.4) - typescript: 5.4.4 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - - supports-color + - bufferutil + - msw + - utf-8-validate + - vite - '@typescript-eslint/types@8.50.0': {} + '@vitest/browser-preview@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': + dependencies: + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true - '@typescript-eslint/typescript-estree@8.50.0(typescript@5.4.4)': + '@vitest/browser-preview@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': dependencies: - '@typescript-eslint/project-service': 8.50.0(typescript@5.4.4) - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.4.4) - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 - debug: 4.4.3 - minimatch: 9.0.9 - semver: 7.8.0 - tinyglobby: 0.2.17 - ts-api-utils: 2.1.0(typescript@5.4.4) - typescript: 5.4.4 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - - supports-color + - bufferutil + - msw + - utf-8-validate + - vite + optional: true - '@typescript-eslint/utils@8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.4.4)': + '@vitest/browser-preview@5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.4.4) - eslint: 9.39.4(jiti@2.6.1) - typescript: 5.4.4 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - - supports-color + - bufferutil + - msw + - utf-8-validate + - vite + optional: true - '@typescript-eslint/visitor-keys@8.50.0': + '@vitest/browser-preview@5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': dependencies: - '@typescript-eslint/types': 8.50.0 - eslint-visitor-keys: 4.2.1 + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true - '@vercel/functions@1.0.2': {} + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true - '@vercel/oidc@3.1.0': {} + '@vitest/browser@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true - '@vitest/expect@4.1.11': + '@vitest/browser@5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.13.6(@types/node@20.19.40)(typescript@5.4.4) + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)' + + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.13.6(@types/node@20.19.40)(typescript@5.5.4) + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)' + + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.13.6(@types/node@20.19.40)(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.13.6(@types/node@22.19.1)(typescript@5.5.4) + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)' - '@vitest/mocker@4.1.11(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))': dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.13.6(@types/node@22.19.1)(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + + '@vitest/mocker@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.13.6(@types/node@24.12.4)(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + + '@vitest/mocker@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 optionalDependencies: msw: 2.13.6(@types/node@20.19.40)(typescript@5.4.4) - vite: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/mocker@4.1.11(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.3 optionalDependencies: msw: 2.13.6(@types/node@20.19.40)(typescript@5.5.4) - vite: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + + '@vitest/mocker@5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.13.6(@types/node@20.19.40)(typescript@5.9.3) + vite: 8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/mocker@4.1.11(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.3 optionalDependencies: msw: 2.13.6(@types/node@22.19.1)(typescript@5.5.4) - vite: 8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/mocker@4.1.11(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.3 optionalDependencies: msw: 2.13.6(@types/node@22.19.1)(typescript@5.9.3) - vite: 8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/pretty-format@4.1.11': + '@vitest/pretty-format@5.0.3': dependencies: tinyrainbow: 3.1.1 - '@vitest/runner@4.1.11': + '@vitest/snapshot@5.0.3': dependencies: - '@vitest/utils': 4.1.11 + '@vitest/pretty-format': 5.0.3 + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 pathe: 2.0.3 - '@vitest/snapshot@4.1.11': + '@vitest/spy@5.0.3': {} + + '@vitest/ui@5.0.3(vitest@5.0.3)': dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 + '@vitest/utils': 5.0.3 + fflate: 0.8.3 + flatted: 3.4.4 pathe: 2.0.3 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/spy@4.1.11': {} - - '@vitest/utils@4.1.11': + '@vitest/utils@5.0.3': dependencies: - '@vitest/pretty-format': 4.1.11 + '@vitest/pretty-format': 5.0.3 convert-source-map: 2.0.0 tinyrainbow: 3.1.1 + '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)': + dependencies: + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.29 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + '@types/node': 20.19.40 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.6.1 + publint: 0.3.25 + terser: 5.44.1 + tsx: 4.21.0 + typescript: 5.4.4 + yaml: 2.9.0 + + '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)': + dependencies: + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.29 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + '@types/node': 20.19.40 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.6.1 + publint: 0.3.25 + terser: 5.44.1 + tsx: 4.21.0 + typescript: 5.5.4 + yaml: 2.9.0 + + '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)': + dependencies: + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.29 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + '@types/node': 20.19.40 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.6.1 + publint: 0.3.25 + terser: 5.44.1 + tsx: 4.21.0 + typescript: 5.9.3 + yaml: 2.9.0 + + '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)': + dependencies: + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.29 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + '@types/node': 22.19.1 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.6.1 + publint: 0.3.25 + terser: 5.44.1 + tsx: 4.21.0 + typescript: 5.5.4 + yaml: 2.9.0 + + '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)': + dependencies: + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.29 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + '@types/node': 22.19.1 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.6.1 + publint: 0.3.25 + terser: 5.44.1 + tsx: 4.21.0 + typescript: 5.9.3 + yaml: 2.9.0 + + '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)': + dependencies: + '@oxc-project/runtime': 0.153.0 + '@oxc-project/types': 0.153.0 + lightningcss: 1.33.0 + postcss: 8.5.29 + yuku-codegen: 0.9.5 + yuku-parser: 0.9.5 + optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + '@types/node': 24.12.4 + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.6.1 + publint: 0.3.25 + terser: 5.44.1 + tsx: 4.21.0 + typescript: 5.9.3 + yaml: 2.9.0 + + '@voidzero-dev/vite-plus-darwin-arm64@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-darwin-x64@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-musl@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-gnu@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.1.0': + optional: true + + '@voidzero-dev/vite-plus-win32-x64-msvc@1.1.0': + optional: true + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -5231,6 +6336,80 @@ snapshots: '@xtuc/long@4.2.2': {} + '@yuku-codegen/binding-android-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.9.5': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.9.5': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.9.5': + optional: true + + '@yuku-codegen/binding-win32-x64@0.9.5': + optional: true + + '@yuku-parser/binding-android-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-darwin-x64@0.9.5': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.9.5': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.9.5': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.9.5': + optional: true + + '@yuku-parser/binding-win32-arm64@0.9.5': + optional: true + + '@yuku-parser/binding-win32-x64@0.9.5': + optional: true + + '@yuku-toolchain/types@0.9.5': {} + a-sync-waterfall@1.0.1: {} abort-controller@3.0.0: @@ -5300,9 +6479,9 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.1: {} + ansi-styles@5.2.0: {} - any-promise@1.3.0: {} + ansi-styles@6.2.1: {} argparse@1.0.10: dependencies: @@ -5310,6 +6489,10 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + array-union@2.1.0: {} asap@2.0.6: {} @@ -5351,13 +6534,6 @@ snapshots: buffer-from@1.1.2: {} - bundle-require@5.1.0(esbuild@0.28.1): - dependencies: - esbuild: 0.28.1 - load-tsconfig: 0.2.5 - - cac@6.7.14: {} - callsites@3.1.0: {} caniuse-lite@1.0.30001810: {} @@ -5373,14 +6549,13 @@ snapshots: chardet@2.1.1: {} - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - chownr@3.0.0: {} chrome-trace-event@1.0.4: {} + cjs-module-lexer@1.4.3: + optional: true + cjs-module-lexer@2.2.0: {} cli-cursor@5.0.0: @@ -5419,16 +6594,10 @@ snapshots: commander@2.20.3: {} - commander@4.1.1: {} - commander@5.1.0: {} concat-map@0.0.1: {} - confbox@0.1.8: {} - - consola@3.4.0: {} - convert-source-map@2.0.0: {} cookie@1.1.1: @@ -5454,6 +6623,8 @@ snapshots: deep-is@0.1.4: {} + dequal@2.0.3: {} + detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -5462,20 +6633,18 @@ snapshots: dependencies: path-type: 4.0.0 + dom-accessibility-api@0.5.16: {} + dotenv@16.4.5: {} dotenv@17.3.1: {} - eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.422: {} emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -5540,13 +6709,6 @@ snapshots: eslint: 9.39.4(jiti@2.6.1) eslint-compat-utils: 0.5.1(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-es-x@9.7.0(eslint@9.39.4(jiti@2.6.1)): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.2 - eslint: 9.39.4(jiti@2.6.1) - eslint-type-tracer: 0.5.2(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-n@18.2.1(eslint@9.39.4(jiti@2.6.1))(ts-declaration-location@1.0.7(typescript@5.4.4))(typescript@5.4.4): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) @@ -5562,10 +6724,6 @@ snapshots: ts-declaration-location: 1.0.7(typescript@5.4.4) typescript: 5.4.4 - eslint-plugin-node-import@1.0.5(eslint@9.39.4(jiti@2.6.1)): - dependencies: - eslint: 9.39.4(jiti@2.6.1) - eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -5576,11 +6734,6 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-type-tracer@0.5.2(eslint@9.39.4(jiti@2.6.1)): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - eslint: 9.39.4(jiti@2.6.1) - eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@4.2.1: {} @@ -5704,6 +6857,8 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + fflate@0.8.3: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -5722,23 +6877,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.0 - rollup: 4.60.3 - flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.4 keyv: 4.5.4 - flatted@3.4.2: {} - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 + flatted@3.4.4: {} formatly@0.3.0: dependencies: @@ -5785,15 +6929,6 @@ snapshots: glob-to-regexp@0.4.1: {} - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -5862,8 +6997,6 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -5905,12 +7038,6 @@ snapshots: isexe@2.0.0: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jest-worker@27.5.1: dependencies: '@types/node': 22.19.1 @@ -5919,7 +7046,7 @@ snapshots: jiti@2.6.1: {} - joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@3.15.2: dependencies: @@ -5975,58 +7102,54 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lightningcss-android-arm64@1.32.0: + lightningcss-android-arm64@1.33.0: optional: true - lightningcss-darwin-arm64@1.32.0: + lightningcss-darwin-arm64@1.33.0: optional: true - lightningcss-darwin-x64@1.32.0: + lightningcss-darwin-x64@1.33.0: optional: true - lightningcss-freebsd-x64@1.32.0: + lightningcss-freebsd-x64@1.33.0: optional: true - lightningcss-linux-arm-gnueabihf@1.32.0: + lightningcss-linux-arm-gnueabihf@1.33.0: optional: true - lightningcss-linux-arm64-gnu@1.32.0: + lightningcss-linux-arm64-gnu@1.33.0: optional: true - lightningcss-linux-arm64-musl@1.32.0: + lightningcss-linux-arm64-musl@1.33.0: optional: true - lightningcss-linux-x64-gnu@1.32.0: + lightningcss-linux-x64-gnu@1.33.0: optional: true - lightningcss-linux-x64-musl@1.32.0: + lightningcss-linux-x64-musl@1.33.0: optional: true - lightningcss-win32-arm64-msvc@1.32.0: + lightningcss-win32-arm64-msvc@1.33.0: optional: true - lightningcss-win32-x64-msvc@1.32.0: + lightningcss-win32-x64-msvc@1.33.0: optional: true - lightningcss@1.32.0: + lightningcss@1.33.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 lint-staged@16.2.7: dependencies: @@ -6047,8 +7170,6 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 - load-tsconfig@0.2.5: {} - loader-runner@4.3.1: {} locate-path@5.0.0: @@ -6075,11 +7196,14 @@ snapshots: long@5.3.2: {} - lru-cache@10.4.3: {} + lru-cache@11.5.3: + optional: true lunr@2.3.9: {} - magic-string@0.30.21: + lz-string@1.5.0: {} + + magic-string@1.4.3: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -6125,17 +7249,12 @@ snapshots: dependencies: minipass: 7.1.3 - mlly@1.8.0: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.1 - module-details-from-path@1.0.4: {} mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} ms@3.0.0-canary.1: {} @@ -6192,6 +7311,32 @@ snapshots: - '@types/node' optional: true + msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 6.0.12(@types/node@20.19.40) + '@mswjs/interceptors': 0.41.6 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.13.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.8 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.6.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + optional: true + msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4): dependencies: '@inquirer/confirm': 6.0.12(@types/node@22.19.1) @@ -6244,20 +7389,40 @@ snapshots: - '@types/node' optional: true + msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 6.0.12(@types/node@24.12.4) + '@mswjs/interceptors': 0.41.6 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.13.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.8 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.6.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + optional: true + mustache@4.2.0: {} mute-stream@3.0.0: optional: true - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - nano-spawn@2.0.0: {} - nanoid@3.3.18: {} + nanoid@3.3.20: {} natural-compare@1.4.0: {} @@ -6292,8 +7457,6 @@ snapshots: asap: 2.0.6 commander: 5.1.0 - object-assign@4.1.1: {} - obug@2.2.1: {} once@1.4.0: @@ -6349,6 +7512,309 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + oxfmt@0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)): + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0) + + oxfmt@0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)): + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) + + oxfmt@0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + + oxfmt@0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)): + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) + + oxfmt@0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + + oxfmt@0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.72.0 + '@oxfmt/binding-android-arm64': 0.72.0 + '@oxfmt/binding-darwin-arm64': 0.72.0 + '@oxfmt/binding-darwin-x64': 0.72.0 + '@oxfmt/binding-freebsd-x64': 0.72.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.72.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.72.0 + '@oxfmt/binding-linux-arm64-gnu': 0.72.0 + '@oxfmt/binding-linux-arm64-musl': 0.72.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.72.0 + '@oxfmt/binding-linux-riscv64-musl': 0.72.0 + '@oxfmt/binding-linux-s390x-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-gnu': 0.72.0 + '@oxfmt/binding-linux-x64-musl': 0.72.0 + '@oxfmt/binding-openharmony-arm64': 0.72.0 + '@oxfmt/binding-win32-arm64-msvc': 0.72.0 + '@oxfmt/binding-win32-ia32-msvc': 0.72.0 + '@oxfmt/binding-win32-x64-msvc': 0.72.0 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + + oxlint-tsgolint@7.0.2003: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 7.0.2003 + '@oxlint-tsgolint/darwin-x64': 7.0.2003 + '@oxlint-tsgolint/linux-arm64': 7.0.2003 + '@oxlint-tsgolint/linux-x64': 7.0.2003 + '@oxlint-tsgolint/win32-arm64': 7.0.2003 + '@oxlint-tsgolint/win32-x64': 7.0.2003 + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0) + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + + oxlint@1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.87.0 + '@oxlint/binding-android-arm64': 1.87.0 + '@oxlint/binding-darwin-arm64': 1.87.0 + '@oxlint/binding-darwin-x64': 1.87.0 + '@oxlint/binding-freebsd-x64': 1.87.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 + '@oxlint/binding-linux-arm-musleabihf': 1.87.0 + '@oxlint/binding-linux-arm64-gnu': 1.87.0 + '@oxlint/binding-linux-arm64-musl': 1.87.0 + '@oxlint/binding-linux-ppc64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-gnu': 1.87.0 + '@oxlint/binding-linux-riscv64-musl': 1.87.0 + '@oxlint/binding-linux-s390x-gnu': 1.87.0 + '@oxlint/binding-linux-x64-gnu': 1.87.0 + '@oxlint/binding-linux-x64-musl': 1.87.0 + '@oxlint/binding-openharmony-arm64': 1.87.0 + '@oxlint/binding-win32-arm64-msvc': 1.87.0 + '@oxlint/binding-win32-ia32-msvc': 1.87.0 + '@oxlint/binding-win32-x64-msvc': 1.87.0 + oxlint-tsgolint: 7.0.2003 + vite-plus: 1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -6373,12 +7839,13 @@ snapshots: p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - package-manager-detector@0.2.11: dependencies: quansync: 0.2.11 + package-manager-detector@1.9.0: + optional: true + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6389,11 +7856,6 @@ snapshots: path-key@3.1.1: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.3 - path-to-regexp@6.3.0: optional: true @@ -6411,34 +7873,23 @@ snapshots: pify@4.0.1: {} - pirates@4.0.6: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.0 - pathe: 2.0.3 + pngjs@7.0.0: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(yaml@2.9.0): + postcss@8.5.29: dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 2.6.1 - postcss: 8.5.26 - tsx: 4.21.0 - yaml: 2.9.0 - - postcss@8.5.26: - dependencies: - nanoid: 3.3.18 + nanoid: 3.3.20 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 prelude-ls@1.2.1: {} prettier@2.8.8: {} - prettier@3.8.1: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 proto3-json-serializer@2.0.2: dependencies: @@ -6458,12 +7909,22 @@ snapshots: '@types/node': 22.19.1 long: 5.3.2 + publint@0.3.25: + dependencies: + '@publint/pack': 0.1.7 + package-manager-detector: 1.9.0 + picocolors: 1.1.1 + sade: 1.8.1 + optional: true + punycode@2.3.1: {} quansync@0.2.11: {} queue-microtask@1.2.3: {} + react-is@17.0.2: {} + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -6471,8 +7932,6 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - readdirp@4.1.2: {} - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -6495,26 +7954,26 @@ snapshots: rfdc@1.4.1: {} - rolldown@1.1.5: + rolldown@1.2.13: dependencies: - '@oxc-project/types': 0.139.0 + '@oxc-project/types': 0.153.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 + '@rolldown/binding-android-arm-eabi': 1.2.13 + '@rolldown/binding-android-arm64': 1.2.13 + '@rolldown/binding-darwin-arm64': 1.2.13 + '@rolldown/binding-darwin-x64': 1.2.13 + '@rolldown/binding-freebsd-x64': 1.2.13 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.13 + '@rolldown/binding-linux-arm64-gnu': 1.2.13 + '@rolldown/binding-linux-arm64-musl': 1.2.13 + '@rolldown/binding-linux-ppc64-gnu': 1.2.13 + '@rolldown/binding-linux-s390x-gnu': 1.2.13 + '@rolldown/binding-linux-x64-gnu': 1.2.13 + '@rolldown/binding-linux-x64-musl': 1.2.13 + '@rolldown/binding-openharmony-arm64': 1.2.13 + '@rolldown/binding-win32-arm64-msvc': 1.2.13 + '@rolldown/binding-win32-x64-msvc': 1.2.13 rollup@4.60.3: dependencies: @@ -6555,6 +8014,11 @@ snapshots: dependencies: tslib: 2.8.1 + sade@1.8.1: + dependencies: + mri: 1.2.0 + optional: true + safer-buffer@2.1.2: {} schema-utils@4.3.3: @@ -6584,10 +8048,14 @@ snapshots: vscode-oniguruma: 1.7.0 vscode-textmate: 8.0.0 - siginfo@2.0.0: {} - signal-exit@4.1.0: {} + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -6599,13 +8067,13 @@ snapshots: smol-toml@1.8.0: {} - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} - source-map-loader@4.0.2(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26)): + source-map-loader@4.0.2(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)): dependencies: iconv-lite: 0.6.3 - source-map-js: 1.2.1 - webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26) + source-map-js: 1.2.2 + webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1) source-map-support@0.5.21: dependencies: @@ -6623,8 +8091,6 @@ snapshots: sprintf-js@1.0.3: {} - stackback@0.0.2: {} - statuses@2.0.2: optional: true @@ -6641,12 +8107,6 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -6672,16 +8132,6 @@ snapshots: strip-json-comments@5.0.3: {} - sucrase@3.35.0: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - glob: 10.5.0 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.6 - ts-interface-checker: 0.1.13 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -6690,11 +8140,11 @@ snapshots: dependencies: has-flag: 4.0.0 - swc-loader@0.2.6(@swc/core@1.15.8)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26)): + swc-loader@0.2.6(@swc/core@1.15.8)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)): dependencies: '@swc/core': 1.15.8 '@swc/counter': 0.1.3 - webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26) + webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1) tagged-tag@1.0.0: optional: true @@ -6711,17 +8161,26 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.6.0(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26)): + terser-webpack-plugin@5.6.0(@swc/core@1.15.8)(esbuild@0.28.1)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.44.1 - webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26) + webpack: 5.106.2(@swc/core@1.15.8)(esbuild@0.28.1) optionalDependencies: '@swc/core': 1.15.8 esbuild: 0.28.1 - postcss: 8.5.26 + + terser-webpack-plugin@5.6.0(esbuild@0.28.1)(webpack@5.106.2(esbuild@0.28.1)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.44.1 + webpack: 5.106.2(esbuild@0.28.1) + optionalDependencies: + esbuild: 0.28.1 terser@5.44.1: dependencies: @@ -6730,23 +8189,13 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - thingies@2.5.0(tslib@2.8.1): dependencies: tslib: 2.8.1 - tinybench@2.9.0: {} - tinybench@4.0.1: {} - tinyexec@0.3.2: {} + tinybench@6.2.2: {} tinyexec@1.3.1: {} @@ -6755,6 +8204,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinypool@2.2.0: {} + tinyrainbow@3.1.1: {} tldts-core@7.0.29: @@ -6769,6 +8220,8 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tough-cookie@6.0.1: dependencies: tldts: 7.0.29 @@ -6780,112 +8233,13 @@ snapshots: dependencies: tslib: 2.8.1 - tree-kill@1.2.2: {} - - ts-api-utils@2.1.0(typescript@5.4.4): - dependencies: - typescript: 5.4.4 - ts-declaration-location@1.0.7(typescript@5.4.4): dependencies: picomatch: 4.0.7 typescript: 5.4.4 - ts-interface-checker@0.1.13: {} - - tsconfck@3.1.1(typescript@5.4.4): - optionalDependencies: - typescript: 5.4.4 - tslib@2.8.1: {} - tsup@8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0): - dependencies: - bundle-require: 5.1.0(esbuild@0.28.1) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.0 - debug: 4.4.3 - esbuild: 0.28.1 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(yaml@2.9.0) - resolve-from: 5.0.0 - rollup: 4.60.3 - source-map: 0.7.6 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.17 - tree-kill: 1.2.2 - optionalDependencies: - '@swc/core': 1.15.8 - postcss: 8.5.26 - typescript: 5.4.4 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tsup@8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0): - dependencies: - bundle-require: 5.1.0(esbuild@0.28.1) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.0 - debug: 4.4.3 - esbuild: 0.28.1 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(yaml@2.9.0) - resolve-from: 5.0.0 - rollup: 4.60.3 - source-map: 0.7.6 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.17 - tree-kill: 1.2.2 - optionalDependencies: - '@swc/core': 1.15.8 - postcss: 8.5.26 - typescript: 5.5.4 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tsup@8.5.1(@swc/core@1.15.8)(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): - dependencies: - bundle-require: 5.1.0(esbuild@0.28.1) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.0 - debug: 4.4.3 - esbuild: 0.28.1 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.26)(tsx@4.21.0)(yaml@2.9.0) - resolve-from: 5.0.0 - rollup: 4.60.3 - source-map: 0.7.6 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.17 - tree-kill: 1.2.2 - optionalDependencies: - '@swc/core': 1.15.8 - postcss: 8.5.26 - typescript: 5.9.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - tsx@4.21.0: dependencies: esbuild: 0.28.1 @@ -6893,15 +8247,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo@2.9.14: - optionalDependencies: - '@turbo/darwin-64': 2.9.14 - '@turbo/darwin-arm64': 2.9.14 - '@turbo/linux-64': 2.9.14 - '@turbo/linux-arm64': 2.9.14 - '@turbo/windows-64': 2.9.14 - '@turbo/windows-arm64': 2.9.14 - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6928,10 +8273,11 @@ snapshots: typescript@5.5.4: {} + typescript@5.6.1-rc: + optional: true + typescript@5.9.3: {} - ufo@1.6.1: {} - uglify-js@3.19.3: optional: true @@ -6969,23 +8315,369 @@ snapshots: uuid@11.1.1: {} - vite-tsconfig-paths@4.3.2(typescript@5.4.4)(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.1(typescript@5.4.4) + validate-npm-package-name@5.0.1: + optional: true + + vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)' + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4)) optionalDependencies: - vite: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 transitivePeerDependencies: - - supports-color + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc + - yaml + + vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)' + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4)) + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc + - yaml + + vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3)) + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc + - yaml + + vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)' + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4)) + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc + - yaml + + vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3)) + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc + - yaml + + vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.153.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3)) + '@vitest/pretty-format': 5.0.3 + '@vitest/snapshot': 5.0.3 + '@vitest/spy': 5.0.3 + '@vitest/utils': 5.0.3 + oxfmt: 0.72.0(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + oxlint: 1.87.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.1.0(@arethetypeswrong/core@0.18.5)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/ui@5.0.3(vitest@5.0.3))(esbuild@0.28.1)(jiti@2.6.1)(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2003 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + vitest: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3)) + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 1.1.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.1.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.1.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.1.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vue-tsc + - yaml - vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0): + vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: - lightningcss: 1.32.0 + lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.26 - rolldown: 1.1.5 + postcss: 8.5.29 + rolldown: 1.2.13 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 20.19.40 @@ -6996,12 +8688,12 @@ snapshots: tsx: 4.21.0 yaml: 2.9.0 - vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0): + vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: - lightningcss: 1.32.0 + lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.26 - rolldown: 1.1.5 + postcss: 8.5.29 + rolldown: 1.2.13 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.19.1 @@ -7012,115 +8704,291 @@ snapshots: tsx: 4.21.0 yaml: 2.9.0 - vitest@4.1.11(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4)) + chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.3 obug: 2.2.1 - pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 + tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.4.4)(yaml@2.9.0)' + why-is-node-running: 3.2.1 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 20.19.40 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) transitivePeerDependencies: - msw - vitest@4.1.11(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4)) + chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.3 obug: 2.2.1 - pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 + tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.1.5(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)' + why-is-node-running: 3.2.1 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 20.19.40 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) transitivePeerDependencies: - msw - vitest@4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3)) + chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.3 obug: 2.2.1 - pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 + tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 20.19.40 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 20.19.40 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.9.3))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 20.19.40 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.4.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 20.19.40 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@20.19.40)(typescript@5.5.4))(vite@8.3.4(@types/node@20.19.40)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)' + why-is-node-running: 3.2.1 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.1 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) transitivePeerDependencies: - msw - vitest@4.1.11(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3)) + chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.3 obug: 2.2.1 - pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 + tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.1.5(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.19.1 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.19.1 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.19.1 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.9.3))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3)(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 3.2.1 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.1 + '@vitest/browser-preview': 5.0.3(msw@2.13.6(@types/node@22.19.1)(typescript@5.5.4))(vite@8.3.4(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(@vitest/browser-preview@5.0.3)(@vitest/ui@5.0.3(vitest@5.0.3))(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: '@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)' + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.12.4 + '@vitest/browser-preview': 5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@arethetypeswrong/core@0.18.5)(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.6.1)(publint@0.3.25)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(msw@2.13.6(@types/node@24.12.4)(typescript@5.9.3))(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) transitivePeerDependencies: - msw @@ -7141,7 +9009,47 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26): + webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.8 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.21.2 + es-module-lexer: 2.3.2 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + loader-runner: 4.3.1 + mime-db: 1.54.0 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.0(@swc/core@1.15.8)(esbuild@0.28.1)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)) + watchpack: 2.5.1 + webpack-sources: 3.4.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + webpack@5.106.2(esbuild@0.28.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -7164,7 +9072,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.0(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26)(webpack@5.106.2(@swc/core@1.15.8)(esbuild@0.28.1)(postcss@8.5.26)) + terser-webpack-plugin: 5.6.0(esbuild@0.28.1)(webpack@5.106.2(esbuild@0.28.1)) watchpack: 2.5.1 webpack-sources: 3.4.1 transitivePeerDependencies: @@ -7190,10 +9098,7 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 + why-is-node-running@3.2.1: {} word-wrap@1.2.5: {} @@ -7205,12 +9110,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.2 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.1 @@ -7219,8 +9118,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.21.3: - optional: true + ws@8.21.3: {} y18n@5.0.8: {} @@ -7265,6 +9163,45 @@ snapshots: yocto-queue@0.1.0: {} + yuku-ast@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + + yuku-codegen@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.9.5 + '@yuku-codegen/binding-darwin-arm64': 0.9.5 + '@yuku-codegen/binding-darwin-x64': 0.9.5 + '@yuku-codegen/binding-freebsd-x64': 0.9.5 + '@yuku-codegen/binding-linux-arm-gnu': 0.9.5 + '@yuku-codegen/binding-linux-arm-musl': 0.9.5 + '@yuku-codegen/binding-linux-arm64-gnu': 0.9.5 + '@yuku-codegen/binding-linux-arm64-musl': 0.9.5 + '@yuku-codegen/binding-linux-x64-gnu': 0.9.5 + '@yuku-codegen/binding-linux-x64-musl': 0.9.5 + '@yuku-codegen/binding-win32-arm64': 0.9.5 + '@yuku-codegen/binding-win32-x64': 0.9.5 + + yuku-parser@0.9.5: + dependencies: + '@yuku-toolchain/types': 0.9.5 + yuku-ast: 0.9.5 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.9.5 + '@yuku-parser/binding-darwin-arm64': 0.9.5 + '@yuku-parser/binding-darwin-x64': 0.9.5 + '@yuku-parser/binding-freebsd-x64': 0.9.5 + '@yuku-parser/binding-linux-arm-gnu': 0.9.5 + '@yuku-parser/binding-linux-arm-musl': 0.9.5 + '@yuku-parser/binding-linux-arm64-gnu': 0.9.5 + '@yuku-parser/binding-linux-arm64-musl': 0.9.5 + '@yuku-parser/binding-linux-x64-gnu': 0.9.5 + '@yuku-parser/binding-linux-x64-musl': 0.9.5 + '@yuku-parser/binding-win32-arm64': 0.9.5 + '@yuku-parser/binding-win32-x64': 0.9.5 + zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ae72bc1f8..44967ea5d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,3 +12,50 @@ trustPolicy: no-downgrade # Ignore the check for packages published more than 30 days ago (pnpm 10.27+) # Useful for older packages that pre-date provenance support trustPolicyIgnoreAfter: 43200 # minutes (30 days) +ignoredBuiltDependencies: + - "@swc/core" + - esbuild + - msw + - protobufjs +overrides: + handlebars: ^4.7.9 + protobufjs: ^7.6.5 + body-parser@>=2.0.0 <2.3.0: ^2.3.0 + brace-expansion@>=3.0.0 <5.0.12: ^5.0.12 + esbuild: 0.28.1 + uuid: 11.1.1 + router@2.2.0>path-to-regexp: 8.4.0 + express@5.2.1>qs: 6.16.0 + qs@>=6.7.0 <6.16.0: ^6.16.0 + tar@>=7.0.0 <7.5.21: ^7.5.21 + tar@>=6.0.0 <7.0.0: ^6.2.1 + flatted@<3.4.2: ^3.4.2 + markdown-it@>=13.0.0 <14.1.1: ^14.1.1 + "@mikro-orm/knex@>=6.0.0 <6.6.14": ^6.6.14 + glob@>=10.2.0 <10.5.0: ^10.5.0 + fast-uri@<3.1.8: ^3.1.8 + linkify-it@<5.0.2: ^5.0.2 + brace-expansion@<1.1.17: ^1.1.17 + ip-address@<10.2.2: ^10.2.2 + brace-expansion@2.1.2: ^2.1.3 + hono@<4.12.34: ^4.12.34 + "@hono/node-server@<1.19.15": ^1.19.15 + nanoid@<3.3.18: ^3.3.18 + postcss@<8.5.23: ^8.5.23 + js-yaml@3: ^3.15.1 + js-yaml@4: ^4.3.1 + "@opentelemetry/core@2": ^2.8.0 + "@ai-sdk/provider-utils": ^4.0.33 + vite@*: "catalog:" + vitest@*: "catalog:" +catalog: + vite: npm:@voidzero-dev/vite-plus-core@1.1.0 + vitest: 5.0.3 + vite-plus: 1.1.0 +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: "*" + vitest: "*" diff --git a/turbo.json b/turbo.json deleted file mode 100644 index 6afc75def..000000000 --- a/turbo.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "tasks": { - "build": { - "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**", "dist/**"], - "env": [] - }, - "yalc:publish": { - "cache": false, - "dependsOn": ["build"], - "outputs": [] - }, - "check:typings": { - "dependsOn": ["^build"], - "outputs": [] - }, - "test": { - "env": [ - "ANTHROPIC_API_KEY", - "BRAINTRUST_API_KEY", - "GEMINI_API_KEY", - "COHERE_API_KEY", - "CURSOR_API_KEY", - "GROQ_API_KEY", - "OPENAI_API_KEY", - "OPENAI_BASE_URL", - "OPENROUTER_API_KEY", - "MISTRAL_API_KEY", - "HUGGINGFACE_API_KEY" - ], - "dependsOn": ["^build", "build"], - "outputs": [] - }, - "test:e2e": { - "cache": false, - "env": [ - "BRAINTRUST_API_KEY", - "BRAINTRUST_E2E_PROJECT_NAME", - "BRAINTRUST_E2E_RUN_CONTEXT_DIR" - ], - "dependsOn": ["^build"], - "outputs": [] - }, - "test:e2e:update": { - "cache": false, - "dependsOn": ["^build"], - "outputs": [] - }, - "test:e2e:record": { - "cache": false, - "env": [ - "ANTHROPIC_API_KEY", - "BRAINTRUST_API_KEY", - "BRAINTRUST_E2E_CASSETTE_MODE", - "BRAINTRUST_E2E_PROJECT_NAME", - "BRAINTRUST_E2E_RUN_CONTEXT_DIR", - "GEMINI_API_KEY", - "COHERE_API_KEY", - "GROQ_API_KEY", - "OPENAI_API_KEY", - "OPENAI_BASE_URL", - "OPENROUTER_API_KEY", - "MISTRAL_API_KEY", - "HUGGINGFACE_API_KEY" - ], - "dependsOn": ["^build"], - "outputs": [] - }, - "lint": { - "outputs": [] - }, - "fix:lint": { - "outputs": [] - }, - "dev": { - "cache": false - }, - "start": { - "cache": false - }, - "clean": { - "cache": false - }, - "watch": {}, - "playground": { - "cache": false, - "passThroughEnv": [ - "ANTHROPIC_API_KEY", - "BRAINTRUST_API_KEY", - "GEMINI_API_KEY", - "COHERE_API_KEY", - "CURSOR_API_KEY", - "GROQ_API_KEY", - "OPENAI_API_KEY", - "OPENROUTER_API_KEY", - "MISTRAL_API_KEY", - "HUGGINGFACE_API_KEY" - ], - "dependsOn": ["^build", "build"] - } - } -} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 000000000..11f91acad --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,167 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + fmt: { + printWidth: 80, + sortImports: false, + sortPackageJson: false, + ignorePatterns: [ + "**/dist/**", + "**/node_modules/**", + ".context/**", + "js/tests/auto-instrumentations/output-*/**", + "patches/**/*.patch", + "e2e/**/__snapshots__/**", + "**/pnpm-lock.yaml", + "**/deno.lock", + "**/generated*_types.ts", + "js/scripts/openapi_zod_client_output_template.hbs", + ".agents/skills/dotagents/**", + ], + }, + lint: { + options: { typeAware: true, typeCheck: true }, + plugins: ["typescript", "unicorn", "node"], + categories: { correctness: "off" }, + ignorePatterns: [ + "**/dist/**", + "**/node_modules/**", + ".context/**", + "**/vendor/**", + "**/generated*_types.ts", + "**/*.test.ts", + "**/*.test.tsx", + "js/src/auto-instrumentations/**", + "js/examples/**", + "js/smoke/**", + "js/tests/**", + "js/scripts/**", + "e2e/**", + "integrations/**", + "dev-packages/**", + "scripts/**", + ], + jsPlugins: ["./js/node_modules/eslint-plugin-n/lib/index.js"], + rules: { + "no-unused-vars": [ + "error", + { + args: "none", + caughtErrors: "none", + varsIgnorePattern: "^_", + ignoreRestSiblings: false, + }, + ], + "prefer-const": "error", + "typescript/no-explicit-any": "warn", + "typescript/no-empty-object-type": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/prefer-as-const": "error", + "typescript/no-array-constructor": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/triple-slash-reference": "error", + "typescript/consistent-type-assertions": [ + "warn", + { assertionStyle: "never" }, + ], + "no-unused-expressions": ["error", { allowShortCircuit: true }], + "n/no-unsupported-features/node-builtins": [ + "error", + { version: ">=22.18.0", allowExperimental: true }, + ], + "n/no-unsupported-features/es-builtins": [ + "error", + { version: ">=22.18.0" }, + ], + "n/no-unsupported-features/es-syntax": [ + "error", + { version: ">=22.18.0" }, + ], + }, + overrides: [ + { + files: ["js/src/**/*.ts", "js/util/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "**/generated_types", + "**/generated_plain_types", + "**/generated_types.ts", + "**/generated_plain_types.ts", + ], + message: + "Use SDK-owned types and validators; generated backend types are compatibility fixtures.", + }, + ], + }, + ], + "no-restricted-properties": [ + "error", + ...["log", "warn", "error", "debug", "info", "trace"].map( + (property) => ({ + object: "console", + property, + message: "Use debugLogger instead of console for SDK logging.", + }), + ), + { + property: "enterWith", + message: + "Use AsyncLocalStorage.run(); enterWith is unsupported in Workers.", + }, + ], + "unicorn/prefer-node-protocol": "error", + }, + }, + { + files: ["js/src/**/*.ts"], + excludeFiles: ["js/src/{node,browser,workerd,edge-light}/index.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "**/generated_types", + "**/generated_plain_types", + "**/generated_types.ts", + "**/generated_plain_types.ts", + ], + message: + "Use SDK-owned types and validators; generated backend types are compatibility fixtures.", + }, + { + group: [ + "./exports", + "./exports.ts", + "../exports", + "../exports.ts", + ], + message: + "Import from the specific implementation module instead of the public barrel.", + }, + ], + }, + ], + }, + }, + { + files: ["js/src/queue.bench.ts"], + rules: { "no-restricted-properties": "off" }, + }, + ], + }, +}); From c5bb877ec1d685aab99528f28cbada11d1b08b57 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 16 Oct 2026 19:35:16 +0200 Subject: [PATCH 16/19] fix(instrumentation): Initialize tracing automatically in bundles (#2630) I fucked up with the vite plus change because we made it more treeshakable but completely fumbled because people had to add an import for auto instrumentation when using bundlers. We now make it so that the bundlers inject the import statement directly. - Bundler integrations now load `braintrust/auto` automatically. Previously, they added tracing hooks but required a separate import to actually collect traces. - Apply this to both standard instrumentation and provider-specific patches, preserving strict mode and source maps. --- .changeset/chilly-ends-appear.md | 5 +++ .../src/worker.ts | 4 -- .../template/app/api/test/route.ts | 1 - .../auto-instrumentations/bundler/plugin.ts | 14 ++++++- .../bundler/webpack-loader.ts | 11 ++++- .../orchestrion-js/transformer.ts | 31 +++++++++++++- js/src/imports.test.ts | 18 ++++---- js/tests/auto-instrumentations/test-bundle.ts | 5 +++ .../transformation.test.ts | 41 +++++++++++++++++++ 9 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 .changeset/chilly-ends-appear.md diff --git a/.changeset/chilly-ends-appear.md b/.changeset/chilly-ends-appear.md new file mode 100644 index 000000000..51e1b5719 --- /dev/null +++ b/.changeset/chilly-ends-appear.md @@ -0,0 +1,5 @@ +--- +"braintrust": patch +--- + +fix(instrumentation): Initialize tracing automatically in bundles diff --git a/e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts b/e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts index 4d509747c..155edbf3f 100644 --- a/e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts +++ b/e2e/scenarios/cloudflare-think-instrumentation/src/worker.ts @@ -1,13 +1,9 @@ -// Bundler transforms and runtime consumers are enabled independently. import * as cloudflareThink from "@cloudflare/think"; import { createOpenAI } from "@ai-sdk/openai"; import { flush, initLogger, wrapCloudflareThink } from "braintrust"; import { tool } from "ai"; import { z } from "zod"; -if (process.env.CLOUDFLARE_THINK_INSTRUMENTATION !== "manual") - await import("braintrust/auto"); - const OPENAI_MODEL = "gpt-5-nano"; type Env = { diff --git a/e2e/scenarios/nextjs-auto-instrumentation/template/app/api/test/route.ts b/e2e/scenarios/nextjs-auto-instrumentation/template/app/api/test/route.ts index 66c3af088..cdc4bb6e1 100644 --- a/e2e/scenarios/nextjs-auto-instrumentation/template/app/api/test/route.ts +++ b/e2e/scenarios/nextjs-auto-instrumentation/template/app/api/test/route.ts @@ -1,4 +1,3 @@ -import "braintrust/auto"; // Registers Braintrust's global instrumentation hooks. import OpenAI from "openai"; import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index 561144130..fcb841c07 100644 --- a/js/src/auto-instrumentations/bundler/plugin.ts +++ b/js/src/auto-instrumentations/bundler/plugin.ts @@ -1,5 +1,6 @@ import { createUnplugin, type UnpluginInstance } from "unplugin"; import { create, type InstrumentationConfig } from "../orchestrion-js"; +import { Transformer } from "../orchestrion-js/transformer"; import { extname, isAbsolute, join, sep } from "path"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; @@ -129,7 +130,12 @@ export const unplugin: UnpluginInstance = browser, }); if (patched !== null) { - return { code: patched, map: null }; + return new Transformer( + moduleName, + moduleVersion ?? "", + normalizedModulePath, + [], + ).transform(patched, isModule ? "esm" : "cjs", "braintrust/auto"); } // If no version found @@ -155,7 +161,11 @@ export const unplugin: UnpluginInstance = try { // Transform the code const moduleType = isModule ? "esm" : "cjs"; - const result = transformer.transform(code, moduleType); + const result = transformer.transform( + code, + moduleType, + "braintrust/auto", + ); return { code: result.code, map: result.map, diff --git a/js/src/auto-instrumentations/bundler/webpack-loader.ts b/js/src/auto-instrumentations/bundler/webpack-loader.ts index 80874304e..be12605f6 100644 --- a/js/src/auto-instrumentations/bundler/webpack-loader.ts +++ b/js/src/auto-instrumentations/bundler/webpack-loader.ts @@ -9,6 +9,7 @@ */ import { create } from "../orchestrion-js"; +import { Transformer } from "../orchestrion-js/transformer"; import { extname, join, sep } from "path"; import { readFileSync } from "fs"; import moduleDetailsFromPath from "module-details-from-path"; @@ -120,7 +121,13 @@ function codeTransformerLoader( browser: options.browser ?? false, }); if (patched !== null) { - return callback(null, patched); + const result = new Transformer( + moduleName, + "", + normalizedModulePath, + [], + ).transform(patched, isModule ? "esm" : "cjs", "braintrust/auto"); + return callback(null, result.code, result.map); } const moduleVersion = getModuleVersion(moduleDetails.basedir); @@ -141,7 +148,7 @@ function codeTransformerLoader( try { const moduleType: ModuleType = isModule ? "esm" : "cjs"; - const result = transformer.transform(code, moduleType); + const result = transformer.transform(code, moduleType, "braintrust/auto"); callback(null, result.code, result.map ?? undefined); } catch (error) { console.warn( diff --git a/js/src/auto-instrumentations/orchestrion-js/transformer.ts b/js/src/auto-instrumentations/orchestrion-js/transformer.ts index 41de794f5..e5d581cf5 100644 --- a/js/src/auto-instrumentations/orchestrion-js/transformer.ts +++ b/js/src/auto-instrumentations/orchestrion-js/transformer.ts @@ -47,7 +47,11 @@ export class Transformer { * Instruments `code` by injecting global tracing hooks around the * target functions defined by this transformer's configs. */ - transform(code: string | Buffer, moduleType: ModuleType): TransformOutput { + transform( + code: string | Buffer, + moduleType: ModuleType, + runtimeImport?: string, + ): TransformOutput { if (Buffer.isBuffer(code)) { code = code.toString(); } @@ -126,6 +130,31 @@ export class Transformer { ); } + if (runtimeImport) { + // Bundlers need the consumers in the application, whereas Node hooks + // register them in their preload. Insert into the AST to preserve both + // directive prologues (especially CJS strict mode) and source mappings. + ast ??= parse(code, { + loc: true, + ranges: true, + raw: true, + module: moduleType === "esm", + }); + const statement = + moduleType === "esm" + ? `import ${JSON.stringify(runtimeImport)};` + : `require(${JSON.stringify(runtimeImport)});`; + const initialization = parse(statement, { module: moduleType === "esm" }); + let index = 0; + while ( + ast.body[index]?.type === "ExpressionStatement" && + typeof ast.body[index].expression.value === "string" + ) { + index++; + } + ast.body.splice(index, 0, ...initialization.body); + } + if (ast) { const file = `${this.moduleName}/${this.filePath}`; const sourceMap = new SourceMapGenerator({ file }); diff --git a/js/src/imports.test.ts b/js/src/imports.test.ts index a81e59237..11ef3190e 100644 --- a/js/src/imports.test.ts +++ b/js/src/imports.test.ts @@ -113,19 +113,17 @@ describe("Import restrictions", () => { const content = fs.readFileSync(filePath, "utf-8"); const lines = content.split("\n"); - // Exception: the Mastra observability patch *generates* CJS source code - // (a string of JavaScript that is appended to @mastra/core's - // dist/mastra/index.cjs). That source legitimately contains - // `require(...)` because it runs inside a CommonJS module's evaluation - // — it isn't a require() call from our SDK code itself. Normalize the - // separator so the exemption matches on Windows (`\\`) too. - const isMastraPatchGenerator = relativePath - .replaceAll(path.sep, "/") - .includes("auto-instrumentations/loader/mastra-observability-patch.ts"); + // These generators emit CJS source containing require() calls for the + // instrumented module to execute, rather than calling require() in the SDK. + // Normalize separators so the exceptions also match on Windows. + const isCommonJsGenerator = [ + "auto-instrumentations/loader/mastra-observability-patch.ts", + "auto-instrumentations/orchestrion-js/transformer.ts", + ].includes(relativePath.replaceAll(path.sep, "/")); lines.forEach((line, index) => { // Check for require() calls - if (/\brequire\s*\(/.test(line) && !isMastraPatchGenerator) { + if (/\brequire\s*\(/.test(line) && !isCommonJsGenerator) { violations.push( `${relativePath}:${index + 1} - Found require() statement: "${line.trim()}"`, ); diff --git a/js/tests/auto-instrumentations/test-bundle.ts b/js/tests/auto-instrumentations/test-bundle.ts index 7b863db6f..2320e8d86 100644 --- a/js/tests/auto-instrumentations/test-bundle.ts +++ b/js/tests/auto-instrumentations/test-bundle.ts @@ -13,6 +13,8 @@ export type Bundler = "esbuild" | "vite" | "rollup"; * Bundles a fixture entry (which imports packages from fixtures/node_modules) * with the Braintrust plugin for the given bundler and writes an ESM bundle to * `outfile`. + * Keep the runtime external so these tests isolate the generated hooks; e2e + * scenarios verify bundled consumer initialization and span creation. */ export async function bundleFixture( bundler: Bundler, @@ -34,6 +36,7 @@ export async function bundleFixture( absWorkingDir: fixturesDir, preserveSymlinks: true, // Don't dereference symlinks platform: "node", + external: ["braintrust/auto"], }); } else if (bundler === "vite") { const { braintrustVitePlugin } = @@ -49,6 +52,7 @@ export async function bundleFixture( outDir: path.dirname(outfile), emptyOutDir: false, minify: false, + rolldownOptions: { external: ["braintrust/auto"] }, }, plugins: [braintrustVitePlugin(options)], logLevel: "error", @@ -59,6 +63,7 @@ export async function bundleFixture( await import("../../src/auto-instrumentations/bundler/rollup.js"); const bundle = await rollup({ input: entryPoint, + external: ["braintrust/auto"], plugins: [ { name: "resolve-fixture-packages", diff --git a/js/tests/auto-instrumentations/transformation.test.ts b/js/tests/auto-instrumentations/transformation.test.ts index ec6ec4c03..371dd3432 100644 --- a/js/tests/auto-instrumentations/transformation.test.ts +++ b/js/tests/auto-instrumentations/transformation.test.ts @@ -13,6 +13,8 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { runInNewContext } from "node:vm"; +import { SourceMapConsumer } from "source-map"; +import { Transformer } from "../../src/auto-instrumentations/orchestrion-js/transformer"; import { create, type InstrumentationConfig, @@ -81,6 +83,7 @@ function transformTestCode( } function expectGlobalHookTransform(output: string): void { + expect(output).toContain("braintrust/auto"); expect(output).toContain("__braintrust_instrumentation_hooks"); expect(output).toContain("braintrust.global-instrumentation-hooks.registry"); expect(output).toContain("braintrust.global-instrumentation-hooks.hook"); @@ -97,6 +100,37 @@ function expectGlobalHookTransform(output: string): void { } describe("Orchestrion Transformation Tests", () => { + it("initializes consumers before CommonJS calls without changing strict mode or source maps", async () => { + const source = + '"use strict";\nfunction request() { return this; }\nmodule.exports = request();'; + const transformer = new Transformer("test-sdk", "1.0.0", "index.cjs", [ + testConfig({ functionName: "request", kind: "Sync" }), + ]); + const result = transformer.transform(source, "cjs", "braintrust/auto"); + const loadedModule = { exports: "not called" }; + const require = vi.fn((id: string) => { + expect(id).toBe("braintrust/auto"); + expect(loadedModule.exports).toBe("not called"); + }); + + Function("require", "module", result.code)(require, loadedModule); + expect(require).toHaveBeenCalledOnce(); + expect(loadedModule.exports).toBeUndefined(); + + const lines = result.code.split("\n"); + const generatedLine = + lines.findIndex((line) => line.startsWith("module.exports")) + 1; + await SourceMapConsumer.with(result.map!, null, (consumer) => { + expect( + consumer.originalPositionFor({ line: generatedLine, column: 0 }), + ).toMatchObject({ + source: "test-sdk/index.cjs", + line: 3, + column: 0, + }); + }); + }); + beforeAll(() => { // Create output directory if (!fs.existsSync(outputDir)) { @@ -705,6 +739,7 @@ describe("Orchestrion Transformation Tests", () => { library: { type: "module" }, }, experiments: { outputModule: true }, + externals: { "braintrust/auto": "module braintrust/auto" }, mode: "development", resolve: { modules: [nodeModulesDir, "node_modules"] }, plugins: [braintrustWebpackPlugin({ browser })], @@ -758,6 +793,7 @@ describe("Orchestrion Transformation Tests", () => { library: { type: "module" }, }, experiments: { outputModule: true }, + externals: { "braintrust/auto": "module braintrust/auto" }, mode: "development", resolve: { modules: [nodeModulesDir, "node_modules"] }, // No plugins — only the loader, mirroring turbopack's constraint @@ -826,6 +862,7 @@ describe("Orchestrion Transformation Tests", () => { experiments: { outputModule: true }, mode: "development", target, + externals: { "braintrust/auto": "module braintrust/auto" }, module: { rules: [ { @@ -842,9 +879,11 @@ describe("Orchestrion Transformation Tests", () => { expect(errors).toHaveLength(0); if (shouldPatch) { + expect(output).toContain("braintrust/auto"); expect(output).toContain("__braintrustObservabilityClass"); expect(output).toContain("@mastra/observability"); } else { + expect(output).not.toContain("braintrust/auto"); expect(output).not.toContain("__braintrustObservabilityClass"); expect(output).not.toContain("node:module"); } @@ -862,6 +901,7 @@ describe("Orchestrion Transformation Tests", () => { experiments: { outputModule: true }, mode: "development", target: "web", + externals: { "braintrust/auto": "module braintrust/auto" }, module: { rules: [ { @@ -878,6 +918,7 @@ describe("Orchestrion Transformation Tests", () => { expect(errors).toHaveLength(0); expect(output).toContain("__btPatchAPIPromise"); + expect(output).toContain("braintrust/auto"); expect(output).toContain("__btParsePatched"); }); }); From eca72fafbb9764e77c0f7c8a38e30578b364e2c2 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 19 Oct 2026 19:49:54 +0200 Subject: [PATCH 17/19] refactor(sdk)!: Rename automatic attachment capture options (#2636) --- .agents/skills/instrumentation/SKILL.md | 2 +- .changeset/shy-parents-return.md | 6 + e2e/helpers/attachment-capture-assertions.ts | 2 +- e2e/helpers/attachment-capture-scenario.mjs | 6 +- .../ai-sdk-instrumentation/scenario.test.ts | 4 +- .../scenario.test.ts | 4 +- .../scenario.test.ts | 2 +- .../scenario.test.ts | 4 +- .../scenario.test.ts | 4 +- .../groq-instrumentation/scenario.test.ts | 4 +- .../openai-instrumentation/scenario.test.ts | 4 +- .../scenario.test.ts | 4 +- integrations/deepseek-harness/README.md | 6 +- .../deepseek-harness/src/index.test.ts | 10 +- integrations/deepseek-harness/src/index.ts | 8 +- js/src/attachment-capture.test.ts | 128 ++++++++++++------ .../providers/ai-sdk-instrumentation.test.ts | 2 +- .../providers/ai-sdk-v7-telemetry.test.ts | 4 +- .../anthropic-instrumentation.test.ts | 4 +- .../elevenlabs-instrumentation.test.ts | 4 +- .../providers/eve-instrumentation.test.ts | 8 +- .../google-genai-instrumentation.test.ts | 6 +- ...ogle-generative-ai-instrumentation.test.ts | 4 +- .../providers/ollama-instrumentation.test.ts | 2 +- .../providers/openai-media.test.ts | 13 +- .../pi-coding-agent-instrumentation.test.ts | 2 +- .../strands-agent-sdk-instrumentation.test.ts | 4 +- .../voyageai-instrumentation.test.ts | 2 +- js/src/logger/core.ts | 37 ++--- js/src/wrappers/groq.test.ts | 4 +- js/tests/provider-wrappers.test.ts | 16 +-- 31 files changed, 183 insertions(+), 127 deletions(-) create mode 100644 .changeset/shy-parents-return.md diff --git a/.agents/skills/instrumentation/SKILL.md b/.agents/skills/instrumentation/SKILL.md index 74e344508..fc3911658 100644 --- a/.agents/skills/instrumentation/SKILL.md +++ b/.agents/skills/instrumentation/SKILL.md @@ -40,7 +40,7 @@ Map the change before editing: - Pass `Error` objects directly as `span.log({ error })` values. The SDK serializes errors correctly, so do not add local helpers that manually turn errors into message/stack strings unless an external API requires a non-`Error` representation. - Log only the useful surface. Prefer narrow, stable payloads over dumping full request/response objects; exclude redundant or overly large data when possible. - Automatic capture of inline multimodal bytes as Braintrust attachments must be opt-in. - Guard attachment creation and binary observation with `BRAINTRUST_CAPTURE_ATTACHMENTS`, and enable them only when its value is `1` or `true`. + Guard attachment creation and binary observation with `BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS`, and enable them only when its value is `1` or `true`. Preserve useful text, metadata, metrics, and remote references when attachment capture is disabled, and omit inline bytes from logged payloads. - Do not modify package READMEs during instrumentation work unless the user specifically requests it or the change corrects outdated information. - We want to limit our instrumentation to operations that are relevant for AI generations and operations (LLMs, embeddings, media generation, ...). Things like creating entities on platforms (CRUD for Workflows of Agent entities) is irrelevant to us. diff --git a/.changeset/shy-parents-return.md b/.changeset/shy-parents-return.md new file mode 100644 index 000000000..e3047202f --- /dev/null +++ b/.changeset/shy-parents-return.md @@ -0,0 +1,6 @@ +--- +"braintrust": major +"@braintrust/deepseek-harness": major +--- + +refactor(sdk)!: Rename automatic attachment capture options diff --git a/e2e/helpers/attachment-capture-assertions.ts b/e2e/helpers/attachment-capture-assertions.ts index ad73b8bce..69b2651db 100644 --- a/e2e/helpers/attachment-capture-assertions.ts +++ b/e2e/helpers/attachment-capture-assertions.ts @@ -20,7 +20,7 @@ export function defineAttachmentCaptureTests(options: { env: { CAPTURE_PACKAGE_NAME: options.packageName, CAPTURE_WRAPPED: String(mode === "wrapped"), - BRAINTRUST_CAPTURE_ATTACHMENTS: "", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "", }, runContext: { variantKey: options.variantKey, diff --git a/e2e/helpers/attachment-capture-scenario.mjs b/e2e/helpers/attachment-capture-scenario.mjs index f8145696f..f2ae7e2a2 100644 --- a/e2e/helpers/attachment-capture-scenario.mjs +++ b/e2e/helpers/attachment-capture-scenario.mjs @@ -103,13 +103,13 @@ export async function runAttachmentCaptureScenario(provider, scenarioUrl, sdk) { } const projectName = scopedName("tmp-luca-attachment-capture"); for (const testCase of captureCases) { - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS = testCase.env; + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS = testCase.env; if (testCase.local) - initLogger({ projectName, captureAttachments: testCase.global }); + initLogger({ projectName, autoCaptureAttachments: testCase.global }); const logger = initLogger({ projectName, setCurrent: !testCase.local, - captureAttachments: testCase.option, + autoCaptureAttachments: testCase.option, }); await logger.traced(request, { name: `attachment-capture-${testCase.name}`, diff --git a/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts index fad2b1ef7..966fcec51 100644 --- a/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/ai-sdk-instrumentation/scenario.test.ts @@ -55,7 +55,7 @@ describeVariants("variants", () => { await runScenarioDir({ entry: scenario.wrapperEntry, env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", AI_SDK_PACKAGE_NAME: scenario.packageName, AI_SDK_OPENAI_PACKAGE_NAME: scenario.openaiModuleName, AI_SDK_SUPPORTS_EVALUATE_STRING_MODEL: String( @@ -117,7 +117,7 @@ describeVariants("variants", () => { await runNodeScenarioDir({ entry: scenario.autoEntry, env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", AI_SDK_PACKAGE_NAME: scenario.packageName, AI_SDK_OPENAI_PACKAGE_NAME: scenario.openaiModuleName, AI_SDK_SUPPORTS_EVALUATE_STRING_MODEL: String( diff --git a/e2e/scenarios/anthropic-instrumentation/scenario.test.ts b/e2e/scenarios/anthropic-instrumentation/scenario.test.ts index 4478ca6d5..bb58bde67 100644 --- a/e2e/scenarios/anthropic-instrumentation/scenario.test.ts +++ b/e2e/scenarios/anthropic-instrumentation/scenario.test.ts @@ -54,7 +54,7 @@ describe.concurrent("variants", () => { await runScenarioDir({ entry: scenario.wrapperEntry, env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", ANTHROPIC_PACKAGE_NAME: scenario.dependencyName, }, runContext: { @@ -81,7 +81,7 @@ describe.concurrent("variants", () => { await runNodeScenarioDir({ entry: scenario.autoEntry, env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", ANTHROPIC_PACKAGE_NAME: scenario.dependencyName, }, nodeArgs: ["--import", "braintrust/hook.mjs"], diff --git a/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts b/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts index 4543c1e43..91546d766 100644 --- a/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts +++ b/e2e/scenarios/elevenlabs-instrumentation/scenario.test.ts @@ -30,7 +30,7 @@ for (const variant of ["elevenlabs-v2", "elevenlabs-v2-latest"]) { ? ["--import", "braintrust/hook.mjs"] : [], env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", ELEVENLABS_PACKAGE_NAME: variant.replace( "elevenlabs-", "elevenlabs-sdk-", diff --git a/e2e/scenarios/google-genai-instrumentation/scenario.test.ts b/e2e/scenarios/google-genai-instrumentation/scenario.test.ts index 51fef3f1e..d649ce6d4 100644 --- a/e2e/scenarios/google-genai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/google-genai-instrumentation/scenario.test.ts @@ -59,7 +59,7 @@ describe.concurrent("variants", () => { entry: scenario.wrapperEntry, env: { GOOGLE_GENAI_PACKAGE_NAME: scenario.dependencyName, - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", }, runContext: { variantKey: scenario.snapshotName, @@ -81,7 +81,7 @@ describe.concurrent("variants", () => { entry: scenario.autoEntry, env: { GOOGLE_GENAI_PACKAGE_NAME: scenario.dependencyName, - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", }, nodeArgs: ["--import", "braintrust/hook.mjs"], runContext: { diff --git a/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts b/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts index 9b5845db7..f1b7061c3 100644 --- a/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/google-generative-ai-instrumentation/scenario.test.ts @@ -35,7 +35,7 @@ describe.concurrent("variants", () => { entry: "scenario.ts", env: { GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName, - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", }, runContext: { originalScenarioDir, @@ -58,7 +58,7 @@ describe.concurrent("variants", () => { nodeArgs: ["--import", "braintrust/hook.mjs"], env: { GOOGLE_GENERATIVE_AI_PACKAGE_NAME: scenario.dependencyName, - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", }, runContext: { originalScenarioDir, diff --git a/e2e/scenarios/groq-instrumentation/scenario.test.ts b/e2e/scenarios/groq-instrumentation/scenario.test.ts index 4ba9b68a1..f16630a8d 100644 --- a/e2e/scenarios/groq-instrumentation/scenario.test.ts +++ b/e2e/scenarios/groq-instrumentation/scenario.test.ts @@ -44,7 +44,7 @@ describe.concurrent("variants", () => { await runScenarioDir({ entry: "scenario.ts", env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", GROQ_AUDIO_FILE: audioFile, GROQ_PACKAGE_NAME: scenario.dependencyName, }, @@ -67,7 +67,7 @@ describe.concurrent("variants", () => { await runNodeScenarioDir({ entry: "scenario.mjs", env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", GROQ_AUDIO_FILE: audioFile, GROQ_PACKAGE_NAME: scenario.dependencyName, }, diff --git a/e2e/scenarios/openai-instrumentation/scenario.test.ts b/e2e/scenarios/openai-instrumentation/scenario.test.ts index b5bbdc767..89d025a91 100644 --- a/e2e/scenarios/openai-instrumentation/scenario.test.ts +++ b/e2e/scenarios/openai-instrumentation/scenario.test.ts @@ -94,7 +94,7 @@ describe.concurrent("variants", () => { await runScenarioDir({ entry: scenario.wrapperEntry, env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", OPENAI_PACKAGE_NAME: scenario.dependencyName, }, runContext: { @@ -117,7 +117,7 @@ describe.concurrent("variants", () => { await runNodeScenarioDir({ entry: scenario.autoEntry, env: { - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", OPENAI_PACKAGE_NAME: scenario.dependencyName, }, nodeArgs: ["--import", "braintrust/hook.mjs"], diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts index 95962813a..904c69ca8 100644 --- a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts @@ -54,7 +54,7 @@ describe.concurrent("variants", () => { entry: scenario.wrapperEntry, env: { STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName, - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", }, runContext: { variantKey: scenario.variantKey, @@ -76,7 +76,7 @@ describe.concurrent("variants", () => { entry: scenario.autoEntry, env: { STRANDS_AGENT_SDK_PACKAGE_NAME: scenario.dependencyName, - BRAINTRUST_CAPTURE_ATTACHMENTS: "true", + BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS: "true", }, nodeArgs: ["--import", "braintrust/hook.mjs"], runContext: { diff --git a/integrations/deepseek-harness/README.md b/integrations/deepseek-harness/README.md index 24f89e2e2..f8e45adfc 100644 --- a/integrations/deepseek-harness/README.md +++ b/integrations/deepseek-harness/README.md @@ -30,10 +30,10 @@ To override the defaults, add the following entry to the profile's ``` The optional configuration fields are `apiKey`, `projectName`, `metadata`, -`orgName`, `appUrl`, and `captureAttachments`. `metadata` is added to every +`orgName`, `appUrl`, and `autoCaptureAttachments`. `metadata` is added to every top-level user-turn trace, and `projectName` defaults to `DeepSeek Harness`. You can set `apiKey` under **Settings > Plugins** in Harness; it is declared as a secret field so the settings UI handles it as a credential. A configured `apiKey` takes precedence over the `BRAINTRUST_API_KEY` environment variable. -Set `captureAttachments: true` to upload Harness images as attachments; when it -is omitted, the `BRAINTRUST_CAPTURE_ATTACHMENTS` environment variable decides. +Set `autoCaptureAttachments: true` to upload Harness images as attachments; when it +is omitted, the `BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS` environment variable decides. diff --git a/integrations/deepseek-harness/src/index.test.ts b/integrations/deepseek-harness/src/index.test.ts index 6e3e03bf5..5f4e00790 100644 --- a/integrations/deepseek-harness/src/index.test.ts +++ b/integrations/deepseek-harness/src/index.test.ts @@ -153,10 +153,10 @@ describe("DeepSeek Harness plugin", () => { }); test("passes the attachment capture setting to its logger", () => { - createContext({ captureAttachments: true }); + createContext({ autoCaptureAttachments: true }); expect(mock.initLogger).toHaveBeenCalledWith( - expect.objectContaining({ captureAttachments: true }), + expect.objectContaining({ autoCaptureAttachments: true }), ); }); @@ -242,8 +242,8 @@ describe("DeepSeek Harness plugin", () => { test.each([true, false])( "processes Harness images with capture=%s", - async (captureAttachments) => { - mock.captureAttachmentsEnabled.mockReturnValue(captureAttachments); + async (autoCaptureAttachments) => { + mock.captureAttachmentsEnabled.mockReturnValue(autoCaptureAttachments); const harness = createContext(); const currentSession = session(); const image = { @@ -288,7 +288,7 @@ describe("DeepSeek Harness plugin", () => { await harness.listener("session/flush")(currentSession); let expectedImagePart: Record; - if (captureAttachments) { + if (autoCaptureAttachments) { expect(harness.readImage).toHaveBeenCalledOnce(); expect(harness.readImage).toHaveBeenCalledWith(image); expect(mock.attachments).toHaveLength(1); diff --git a/integrations/deepseek-harness/src/index.ts b/integrations/deepseek-harness/src/index.ts index 128feb88f..cce1f6c4a 100644 --- a/integrations/deepseek-harness/src/index.ts +++ b/integrations/deepseek-harness/src/index.ts @@ -52,9 +52,9 @@ export interface Config { appUrl?: string; /** * Capture inline Harness images as attachments. - * Falls back to BRAINTRUST_CAPTURE_ATTACHMENTS when omitted. + * Falls back to BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS when omitted. */ - captureAttachments?: boolean; + autoCaptureAttachments?: boolean; } export const Config: z = z.object({ @@ -63,7 +63,7 @@ export const Config: z = z.object({ metadata: z.dict(z.any()), orgName: z.string(), appUrl: z.string(), - captureAttachments: z.boolean(), + autoCaptureAttachments: z.boolean(), }); type TokenMetrics = Record & { @@ -380,7 +380,7 @@ export function apply(ctx: Context, config: Config = {}): void { projectName: config.projectName ?? DEFAULT_PROJECT_NAME, ...(config.orgName ? { orgName: config.orgName } : {}), ...(config.appUrl ? { appUrl: config.appUrl } : {}), - captureAttachments: config.captureAttachments, + autoCaptureAttachments: config.autoCaptureAttachments, setCurrent: false, }); const turns = new Map>(); diff --git a/js/src/attachment-capture.test.ts b/js/src/attachment-capture.test.ts index d93f4873c..3ddca3167 100644 --- a/js/src/attachment-capture.test.ts +++ b/js/src/attachment-capture.test.ts @@ -44,15 +44,15 @@ beforeAll(() => { return _exportsForTestingOnly.simulateLoginForTests(); }); beforeEach(() => { - vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", undefined); - state.captureAttachments = undefined; + vi.stubEnv("BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS", undefined); + state.autoCaptureAttachments = undefined; state.currentLogger = undefined; background = _exportsForTestingOnly.useTestBackgroundLogger(); }); afterEach(async () => { await background.drain(); _exportsForTestingOnly.clearTestBackgroundLogger(); - state.captureAttachments = undefined; + state.autoCaptureAttachments = undefined; state.currentLogger = undefined; vi.restoreAllMocks(); vi.unstubAllEnvs(); @@ -112,11 +112,14 @@ it.each([false, true])( it.each([false, true])( "applies the owning policy to AI SDK tool-result media (%s)", async (captureAttachments) => { - initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: !captureAttachments, + }); const local = initLogger({ ...loggerOptions, setCurrent: false, - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const media = ["image-data", "file-data", "media"].map((type) => ({ type, @@ -167,7 +170,10 @@ it.each([false, true])( it.each([false, true])( "processes AI SDK media throughout output containers with capture=%s", (captureAttachments) => { - initLogger({ ...loggerOptions, captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: captureAttachments, + }); const readBytes = vi.fn(() => "AQID"); const generatedFile = { mediaType: "image/png", @@ -238,7 +244,7 @@ it.each([false, true])( const logger = initLogger({ ...loggerOptions, setCurrent: false, - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const processor = new OpenAIAgentsTraceProcessor({ logger }); const trace = { @@ -274,7 +280,10 @@ it.each([false, true])( }; await processor.onTraceStart(trace); await processor.onSpanStart(span); - initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: !captureAttachments, + }); await processor.onSpanEnd(span); const rows = await background.drain(); const output = rows.find( @@ -309,7 +318,7 @@ it.each([false, true])( const local = initLogger({ ...loggerOptions, setCurrent: false, - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const span = local.startSpan({ name: "streamed media" }); const part = { @@ -333,7 +342,10 @@ it.each([false, true])( startTime: Date.now() / 1000, }), ).toBe(true); - initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: !captureAttachments, + }); for await (const chunk of result.fullStream) expect(chunk).toBe(part); const rows = await background.drain(); expect(rows.find((row) => "output" in row)).toHaveProperty("output", { @@ -356,7 +368,7 @@ it.each([false, true])( const local = initLogger({ ...loggerOptions, setCurrent: false, - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const processor = new OpenAIAgentsTraceProcessor({ logger: local }); const trace = { @@ -382,7 +394,10 @@ it.each([false, true])( : { type, input: audio, output: "Hello" }, }; await processor.onSpanStart(span); - initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: !captureAttachments, + }); await processor.onSpanEnd(span); const payload = (await background.drain()).find((row) => "output" in row); expect(payload).toHaveProperty(type === "speech" ? "output" : "input", { @@ -402,11 +417,14 @@ it.each([ ])( "applies capture=%s to AI SDK image-edit inputs and masks (URL object: %s)", async (captureAttachments, maskAsURL) => { - initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: !captureAttachments, + }); const local = initLogger({ ...loggerOptions, setCurrent: false, - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const data = new URL("data:image/png;base64,AQID"); const remote = new URL("https://example.com/image.png"); @@ -458,7 +476,7 @@ it.each([ ); it("preserves AI SDK image-edit inputs when conversion fails", () => { - initLogger({ ...loggerOptions, captureAttachments: true }); + initLogger({ ...loggerOptions, autoCaptureAttachments: true }); const prompt = { images: ["not valid base64!"], mask: "not valid base64!", @@ -474,7 +492,10 @@ it("preserves AI SDK image-edit inputs when conversion fails", () => { it.each([false, true])( "applies capture=%s to AI SDK content parts containing data URL objects", (captureAttachments) => { - initLogger({ ...loggerOptions, captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: captureAttachments, + }); const data = new URL("data:image/png;base64,AQID"); const content = [ { type: "image", image: data }, @@ -505,7 +526,7 @@ it.each([false, true])( ); it("keeps explicit AI SDK attachments with capture disabled", () => { - initLogger({ ...loggerOptions, captureAttachments: false }); + initLogger({ ...loggerOptions, autoCaptureAttachments: false }); const external = new ExternalAttachment({ url: "s3://bucket/image.png", filename: "image.png", @@ -525,8 +546,14 @@ it("keeps explicit AI SDK attachments with capture disabled", () => { it.each([true, false])( "explicit %s overrides an opposing environment setting", (captureAttachments) => { - vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", String(!captureAttachments)); - const logger = initLogger({ ...loggerOptions, captureAttachments }); + vi.stubEnv( + "BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS", + String(!captureAttachments), + ); + const logger = initLogger({ + ...loggerOptions, + autoCaptureAttachments: captureAttachments, + }); expect(_internalCaptureAttachmentsEnabled(logger)).toBe(captureAttachments); expect(_internalCaptureAttachmentsEnabled()).toBe(captureAttachments); initLogger(loggerOptions); @@ -535,30 +562,30 @@ it.each([true, false])( ); it("local loggers use the environment, and never inherit or update the global override", () => { - initLogger({ ...loggerOptions, captureAttachments: true }); + initLogger({ ...loggerOptions, autoCaptureAttachments: true }); const local = initLogger({ ...loggerOptions, setCurrent: false }); expect(_internalCaptureAttachmentsEnabled(local)).toBe(false); initLogger({ ...loggerOptions, setCurrent: false, - captureAttachments: false, + autoCaptureAttachments: false, }); expect(_internalCaptureAttachmentsEnabled()).toBe(true); - vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", "true"); - initLogger({ ...loggerOptions, captureAttachments: false }); + vi.stubEnv("BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS", "true"); + initLogger({ ...loggerOptions, autoCaptureAttachments: false }); expect(_internalCaptureAttachmentsEnabled(local)).toBe(true); }); it("isolates SDK state defaults", () => { const other = new BraintrustState({}); - initLogger({ ...loggerOptions, captureAttachments: true }); + initLogger({ ...loggerOptions, autoCaptureAttachments: true }); const otherLogger = initLogger({ ...loggerOptions, state: other, - captureAttachments: false, + autoCaptureAttachments: false, }); expect(_internalCaptureAttachmentsEnabled(otherLogger)).toBe(false); - expect(other.captureAttachments).toBe(false); + expect(other.autoCaptureAttachments).toBe(false); expect(_internalCaptureAttachmentsEnabled()).toBe(true); }); @@ -566,18 +593,18 @@ it("keeps concurrent local traces and descendants isolated after global replacem const enabled = initLogger({ ...loggerOptions, setCurrent: false, - captureAttachments: true, + autoCaptureAttachments: true, }); const disabled = initLogger({ ...loggerOptions, setCurrent: false, - captureAttachments: false, + autoCaptureAttachments: false, }); await Promise.all( [enabled, disabled].map(async (logger, index) => { await logger.traced(async (parent) => { await Promise.resolve(); - initLogger({ ...loggerOptions, captureAttachments: index !== 0 }); + initLogger({ ...loggerOptions, autoCaptureAttachments: index !== 0 }); await parent.traced(async () => { await Promise.resolve(); const output = processInputAttachments(inlineImage); @@ -591,7 +618,7 @@ it("keeps concurrent local traces and descendants isolated after global replacem }); it("does not decode inline media, read local images, or evaluate generated binary getters when disabled", () => { - initLogger({ ...loggerOptions, captureAttachments: false }); + initLogger({ ...loggerOptions, autoCaptureAttachments: false }); const decode = vi.spyOn(globalThis, "atob"); const stat = vi.spyOn(iso, "statSync"); const binary = vi.fn(() => { @@ -644,7 +671,10 @@ it("does not decode inline media, read local images, or evaluate generated binar }); it("preserves explicit attachments and remote references with capture disabled", async () => { - const logger = initLogger({ ...loggerOptions, captureAttachments: false }); + const logger = initLogger({ + ...loggerOptions, + autoCaptureAttachments: false, + }); const attachment = new Attachment({ data: new Blob(["explicit"]), filename: "explicit.txt", @@ -675,7 +705,7 @@ it("preserves explicit attachments and remote references with capture disabled", }); it("preserves a deferred instrumentation policy when a different logger supplies the span parent", async () => { - initLogger({ ...loggerOptions, captureAttachments: true }); + initLogger({ ...loggerOptions, autoCaptureAttachments: true }); const args = { name: "deferred", [CAPTURE_ATTACHMENTS]: false }; const deferred = startSpan(args); const child = deferred.startSpan(); @@ -688,7 +718,10 @@ it("preserves a deferred instrumentation policy when a different logger supplies it.each([false, true])( "preserves AI SDK application JSON that resembles media (capture=%s)", async (captureAttachments) => { - const logger = initLogger({ ...loggerOptions, captureAttachments }); + const logger = initLogger({ + ...loggerOptions, + autoCaptureAttachments: captureAttachments, + }); const object = { type: "file", data: "report text" }; const toolCall = { type: "tool-call", @@ -758,7 +791,7 @@ it.each([false, true])( async (captureAttachments) => { const logger = initLogger({ ...loggerOptions, - captureAttachments, + autoCaptureAttachments: captureAttachments, setCurrent: false, }); const processor = new OpenAIAgentsTraceProcessor({ logger }); @@ -787,7 +820,10 @@ it.each([false, true])( }; await processor.onTraceStart(trace); await processor.onSpanStart(span); - initLogger({ ...loggerOptions, captureAttachments: !captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: !captureAttachments, + }); await processor.onSpanEnd(span); const rows = await background.drain(); const row = rows.find((row) => "output" in row && row.output !== undefined); @@ -820,7 +856,10 @@ it.each([false, true])( it.each([false, true])( "preserves custom Agents JSON without mutating SDK span data (%s)", async (captureAttachments) => { - const logger = initLogger({ ...loggerOptions, captureAttachments }); + const logger = initLogger({ + ...loggerOptions, + autoCaptureAttachments: captureAttachments, + }); const processor = new OpenAIAgentsTraceProcessor({ logger }); const trace = { type: "trace" as const, @@ -859,7 +898,10 @@ it.each([false, true])( it.each([false, true])( "applies capture policy to Chat Completions audio (%s)", async (captureAttachments) => { - initLogger({ ...loggerOptions, captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: captureAttachments, + }); const audio = { data: "AQID", transcript: "Hello", id: "audio-1" }; const result = { choices: [ @@ -892,7 +934,10 @@ it.each([false, true])( it.each([false, true])( "handles raw base64 chat files without changing references (capture=%s)", async (captureAttachments) => { - initLogger({ ...loggerOptions, captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: captureAttachments, + }); const messages = [ { role: "user", @@ -940,7 +985,10 @@ it.each([false, true])( it.each([false, true])( "preserves parsed OpenAI output and tool arguments that resemble media (capture=%s)", async (captureAttachments) => { - initLogger({ ...loggerOptions, captureAttachments }); + initLogger({ + ...loggerOptions, + autoCaptureAttachments: captureAttachments, + }); const parsed = { type: "file", data: "report text" }; const parsedArguments = { type: "input_image", @@ -997,7 +1045,7 @@ it.each([ ] as const)( "uses the requested %s format for generated chat audio", async (format, contentType, extension) => { - initLogger({ ...loggerOptions, captureAttachments: true }); + initLogger({ ...loggerOptions, autoCaptureAttachments: true }); const audio = { data: "AQID", transcript: "Hello", id: "audio-1" }; const choices = [ { index: 0, message: { role: "assistant", content: null, audio } }, diff --git a/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts index f6dfbcdce..bfa4741ea 100644 --- a/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-instrumentation.test.ts @@ -16,7 +16,7 @@ vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(), getEnv: vi.fn((name: string) => - name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, + name === "BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS" ? "true" : undefined, ), newAsyncLocalStorage: () => ({ getStore: () => undefined, diff --git a/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts b/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts index e33ae78bb..05188299c 100644 --- a/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts +++ b/js/src/instrumentation/providers/ai-sdk-v7-telemetry.test.ts @@ -55,7 +55,7 @@ describe("braintrustAISDKTelemetry", () => { initLogger({ projectName: "tmp-luca-telemetry-capture", projectId: "test-project-id", - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const telemetry = braintrustAISDKTelemetry(); const callId = "delayed-media"; @@ -63,7 +63,7 @@ describe("braintrustAISDKTelemetry", () => { initLogger({ projectName: "tmp-luca-telemetry-capture", projectId: "test-project-id", - captureAttachments: !captureAttachments, + autoCaptureAttachments: !captureAttachments, }); const messages = [ { diff --git a/js/src/instrumentation/providers/anthropic-instrumentation.test.ts b/js/src/instrumentation/providers/anthropic-instrumentation.test.ts index 3ffcb6245..a03cfc64b 100644 --- a/js/src/instrumentation/providers/anthropic-instrumentation.test.ts +++ b/js/src/instrumentation/providers/anthropic-instrumentation.test.ts @@ -5,7 +5,7 @@ vi.mock("#braintrust-platform", () => ({ default: { newTracingChannel: vi.fn(), getEnv: vi.fn((name: string) => - name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, + name === "BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS" ? "true" : undefined, ), }, })); @@ -28,7 +28,7 @@ vi.mock("../../logger/core", async () => { const { default: iso } = await import("#braintrust-platform"); return { _internalCaptureAttachmentsEnabled: () => - iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", + iso.getEnv("BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS") === "true", withCurrent: (_span: unknown, callback: () => unknown) => callback(), startSpan: vi.fn(() => ({ log: vi.fn(), diff --git a/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts b/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts index b156cb297..ed1f5f34e 100644 --- a/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts +++ b/js/src/instrumentation/providers/elevenlabs-instrumentation.test.ts @@ -31,7 +31,7 @@ describe("ElevenLabs instrumentation", () => { beforeEach(() => { logger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - captureAttachments: true, + autoCaptureAttachments: true, projectName: "tmp-luca-elevenlabs-unit", projectId: "test-project-id", }); @@ -46,7 +46,7 @@ describe("ElevenLabs instrumentation", () => { initLogger({ projectName: "tmp-luca-elevenlabs-unit", projectId: "test-project-id", - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const bytes = new Uint8Array([0, 1, 2, 3]); const stream = new ReadableStream({ diff --git a/js/src/instrumentation/providers/eve-instrumentation.test.ts b/js/src/instrumentation/providers/eve-instrumentation.test.ts index 682c961af..8f7442763 100644 --- a/js/src/instrumentation/providers/eve-instrumentation.test.ts +++ b/js/src/instrumentation/providers/eve-instrumentation.test.ts @@ -63,7 +63,7 @@ describe("braintrustEveInstrumentation", () => { initLogger({ projectName: "eve-instrumentation.test.ts", projectId: "test-project-id", - captureAttachments: false, + autoCaptureAttachments: false, }); }); @@ -1146,12 +1146,12 @@ describe("braintrustEveInstrumentation", () => { "applies the persisted or current policy to model media and preserves action JSON ($option, $env, legacy: $legacy)", async ({ option, env, enabled, legacy }) => { const captureAttachments = legacy ? !enabled : enabled; - vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", env); + vi.stubEnv("BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS", env); const logger = initLogger({ projectName: "eve-instrumentation.test.ts", projectId: "test-project-id", setCurrent: false, - captureAttachments: option, + autoCaptureAttachments: option, }); const provider = braintrustEveInstrumentation(); const actionContext = providerContext(); @@ -1172,7 +1172,7 @@ describe("braintrustEveInstrumentation", () => { initLogger({ projectName: "eve-instrumentation.test.ts", projectId: "test-project-id", - captureAttachments: !enabled, + autoCaptureAttachments: !enabled, }); const persistedTurn = { ...(turnContext.state.get() as Record), diff --git a/js/src/instrumentation/providers/google-genai-instrumentation.test.ts b/js/src/instrumentation/providers/google-genai-instrumentation.test.ts index fdc776961..12a14d90b 100644 --- a/js/src/instrumentation/providers/google-genai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-genai-instrumentation.test.ts @@ -27,7 +27,7 @@ vi.mock("#braintrust-platform", () => ({ }), newTracingChannel: vi.fn(), getEnv: vi.fn((name: string) => - name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, + name === "BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS" ? "true" : undefined, ), }, })); @@ -44,7 +44,7 @@ vi.mock("../../logger/core", async () => { const { default: iso } = await import("#braintrust-platform"); return { _internalCaptureAttachmentsEnabled: () => - iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", + iso.getEnv("BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS") === "true", BaseAttachment: class {}, CAPTURE_ATTACHMENTS: Symbol.for("braintrust.captureAttachments"), startSpan: vi.fn(() => ({ @@ -1116,7 +1116,7 @@ describe("registerGoogleGenAIInstrumentation", () => { const getEnv = vi.mocked(iso.getEnv); const originalGetEnv = getEnv.getMockImplementation()!; getEnv.mockImplementation((name) => - name === "BRAINTRUST_CAPTURE_ATTACHMENTS" + name === "BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS" ? String(captureAttachments) : undefined, ); diff --git a/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts b/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts index d9abb2e10..d96ba2c57 100644 --- a/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/google-generative-ai-instrumentation.test.ts @@ -23,7 +23,7 @@ describe("Google Generative AI instrumentation", () => { beforeEach(() => { logger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - captureAttachments: true, + autoCaptureAttachments: true, projectName: "tmp-luca-google-generative-ai-tests", projectId: "test-project-id", }); @@ -374,7 +374,7 @@ describe("Google Generative AI instrumentation", () => { initLogger({ projectName: "tmp-luca-google-generative-ai-tests", projectId: "test-project-id", - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const inlineData = { data: "aGVsbG8=", mimeType: "image/png" }; const request = [{ inlineData }]; diff --git a/js/src/instrumentation/providers/ollama-instrumentation.test.ts b/js/src/instrumentation/providers/ollama-instrumentation.test.ts index 802c1127e..209b070da 100644 --- a/js/src/instrumentation/providers/ollama-instrumentation.test.ts +++ b/js/src/instrumentation/providers/ollama-instrumentation.test.ts @@ -19,7 +19,7 @@ import { } from "./ollama-instrumentation"; configureNode(); -beforeEach(() => vi.stubEnv("BRAINTRUST_CAPTURE_ATTACHMENTS", "true")); +beforeEach(() => vi.stubEnv("BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS", "true")); afterEach(() => vi.unstubAllEnvs()); describe("Ollama instrumentation extraction", () => { diff --git a/js/src/instrumentation/providers/openai-media.test.ts b/js/src/instrumentation/providers/openai-media.test.ts index b97205c73..9fee556fb 100644 --- a/js/src/instrumentation/providers/openai-media.test.ts +++ b/js/src/instrumentation/providers/openai-media.test.ts @@ -8,7 +8,7 @@ import { openAIChannels } from "./openai-channels"; configureNode(); registerInstrumentation(); const originalAutoCaptureAttachments = - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; let background: ReturnType< typeof _exportsForTestingOnly.useTestBackgroundLogger >; @@ -16,7 +16,7 @@ beforeAll(async () => { await _exportsForTestingOnly.simulateLoginForTests(); }); beforeEach(() => { - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS = "true"; + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS = "true"; background = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ projectName: "tmp-luca-openai-media-tests", @@ -25,9 +25,10 @@ beforeEach(() => { }); afterEach(() => { if (originalAutoCaptureAttachments === undefined) { - delete process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; + delete process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; } else { - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS = originalAutoCaptureAttachments; + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS = + originalAutoCaptureAttachments; } _exportsForTestingOnly.clearTestBackgroundLogger(); }); @@ -130,7 +131,7 @@ it("does not upload incomplete speech and preserves response identity", async () }); it("does not capture speech response bytes without attachment opt-in", async () => { - delete process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; + delete process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; const response = new Response(new Uint8Array([1, 2, 3]), { headers: { "content-type": "audio/mpeg" }, }); @@ -153,7 +154,7 @@ it("does not capture speech response bytes without attachment opt-in", async () }); it("does not read media upload values without attachment opt-in", async () => { - delete process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; + delete process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; let reads = 0; const image = { name: "input.png", diff --git a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts index a8c18190a..93dc3331c 100644 --- a/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts +++ b/js/src/instrumentation/providers/pi-coding-agent-instrumentation.test.ts @@ -36,7 +36,7 @@ vi.mock("../../logger/core", async () => { const { default: iso } = await import("#braintrust-platform"); return { _internalCaptureAttachmentsEnabled: () => - iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS") === "true", + iso.getEnv("BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS") === "true", CAPTURE_ATTACHMENTS: Symbol.for("braintrust.captureAttachments"), BaseAttachment: class {}, _internalStartSpan: (...args: unknown[]) => mockStartSpan(...args), diff --git a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts index 351bc24c6..4672a5a7c 100644 --- a/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts +++ b/js/src/instrumentation/providers/strands-agent-sdk-instrumentation.test.ts @@ -32,7 +32,7 @@ const { mockWithCurrent, mockNewAsyncLocalStorage, mockStartSpan } = vi.hoisted( vi.mock("#braintrust-platform", () => ({ default: { getEnv: vi.fn((name: string) => - name === "BRAINTRUST_CAPTURE_ATTACHMENTS" ? "true" : undefined, + name === "BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS" ? "true" : undefined, ), newAsyncLocalStorage: mockNewAsyncLocalStorage, newTracingChannel: vi.fn(), @@ -338,7 +338,7 @@ describe("registerStrandsAgentSDKInstrumentation", () => { "converts media from %s to one attachment shared by agent and model spans", async (_description, bytes, captureAttachments) => { vi.mocked(iso.getEnv).mockImplementation((name) => - name === "BRAINTRUST_CAPTURE_ATTACHMENTS" + name === "BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS" ? String(captureAttachments) : undefined, ); diff --git a/js/src/instrumentation/providers/voyageai-instrumentation.test.ts b/js/src/instrumentation/providers/voyageai-instrumentation.test.ts index 83b956a2f..bcbb9a991 100644 --- a/js/src/instrumentation/providers/voyageai-instrumentation.test.ts +++ b/js/src/instrumentation/providers/voyageai-instrumentation.test.ts @@ -31,7 +31,7 @@ describe("registerVoyageAIInstrumentation", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - captureAttachments: true, + autoCaptureAttachments: true, projectName: "voyageai-instrumentation.test.ts", projectId: "test-project-id", }); diff --git a/js/src/logger/core.ts b/js/src/logger/core.ts index 9b9c14abf..72668f642 100644 --- a/js/src/logger/core.ts +++ b/js/src/logger/core.ts @@ -667,9 +667,10 @@ export const CAPTURE_ATTACHMENTS: unique symbol = Symbol.for( "braintrust.captureAttachments", ); -function captureAttachmentsFromEnv(): boolean { +function autoCaptureAttachmentsFromEnv(): boolean { return ["1", "true"].includes( - iso.getEnv("BRAINTRUST_CAPTURE_ATTACHMENTS")?.trim().toLowerCase() ?? "", + iso.getEnv("BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS")?.trim().toLowerCase() ?? + "", ); } @@ -683,7 +684,7 @@ export function _internalCaptureAttachmentsEnabled(parent?: object): boolean { const policy = owner && Reflect.get(owner, CAPTURE_ATTACHMENTS); return typeof policy === "boolean" ? policy - : (_globalState?.captureAttachments ?? captureAttachmentsFromEnv()); + : (_globalState?.autoCaptureAttachments ?? autoCaptureAttachmentsFromEnv()); } export class BraintrustState { @@ -709,7 +710,7 @@ export class BraintrustState { public loggedIn: boolean = false; public gitMetadataSettings?: GitMetadataSettings; /** @internal Default for instrumentation without a logger-specific policy. */ - public captureAttachments?: boolean; + public autoCaptureAttachments?: boolean; public debugLogLevel?: DebugLogLevel; private debugLogLevelConfigured = false; @@ -2759,7 +2760,7 @@ export class Logger implements Exportable { private lastStartTime: number; private lazyId: LazyValue; private calledStartSpan: boolean; - private readonly captureAttachments: boolean | undefined; + private readonly autoCaptureAttachments: boolean | undefined; // For type identification. public kind = "logger" as const; @@ -2770,7 +2771,7 @@ export class Logger implements Exportable { options: { computeMetadataArgs?: ProjectMetadataArgs; linkArgs?: LinkArgs; - captureAttachments?: boolean; + autoCaptureAttachments?: boolean; } = {}, ) { this.lazyMetadata = lazyMetadata; @@ -2779,7 +2780,7 @@ export class Logger implements Exportable { this.lastStartTime = getCurrentUnixTimestamp(); this.lazyId = new LazyValue(() => this.id); this.calledStartSpan = false; - this.captureAttachments = options.captureAttachments; + this.autoCaptureAttachments = options.autoCaptureAttachments; this.state = state; } @@ -2807,7 +2808,7 @@ export class Logger implements Exportable { Object.defineProperty(this.prototype, CAPTURE_ATTACHMENTS, { configurable: true, get(this: Logger): boolean { - return this.captureAttachments ?? captureAttachmentsFromEnv(); + return this.autoCaptureAttachments ?? autoCaptureAttachmentsFromEnv(); }, }); } @@ -4093,14 +4094,14 @@ async function computeLoggerMetadata( export type InitLoggerOptions = FullLoginOptions & { /** - * Capture inline media as attachments in instrumentation. Explicit booleans override - * BRAINTRUST_CAPTURE_ATTACHMENTS (1 or true); capture is otherwise disabled. + * Automatically capture inline media as attachments in instrumentation. Explicit booleans override + * BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS (1 or true); capture is otherwise disabled. * With setCurrent: false, this logger and its child spans have an independent policy * and an omitted option uses the environment, not the global logger's setting. * Current loggers also update the state's default when this option is supplied; * omitting it preserves that default. Explicitly logged attachments are unaffected. */ - captureAttachments?: boolean; + autoCaptureAttachments?: boolean; projectName?: string; projectId?: string; _createInProjectGroup?: string; @@ -4174,13 +4175,13 @@ export function initLogger(options: Readonly = {}): Logger { ); const setCurrent = options.setCurrent ?? true; - if (setCurrent && options.captureAttachments !== undefined) { - state.captureAttachments = options.captureAttachments; + if (setCurrent && options.autoCaptureAttachments !== undefined) { + state.autoCaptureAttachments = options.autoCaptureAttachments; } const ret = new Logger(state, lazyMetadata, { - captureAttachments: - options.captureAttachments ?? - (setCurrent ? state.captureAttachments : undefined), + autoCaptureAttachments: + options.autoCaptureAttachments ?? + (setCurrent ? state.autoCaptureAttachments : undefined), computeMetadataArgs, linkArgs, }); @@ -6137,8 +6138,8 @@ export class SpanImpl implements Span { Object.defineProperty(this, CAPTURE_ATTACHMENTS, { value: args[CAPTURE_ATTACHMENTS] ?? - args.state.captureAttachments ?? - captureAttachmentsFromEnv(), + args.state.autoCaptureAttachments ?? + autoCaptureAttachmentsFromEnv(), enumerable: true, configurable: true, writable: true, diff --git a/js/src/wrappers/groq.test.ts b/js/src/wrappers/groq.test.ts index d777ae052..1265052c0 100644 --- a/js/src/wrappers/groq.test.ts +++ b/js/src/wrappers/groq.test.ts @@ -31,7 +31,7 @@ describe("groq wrapper", () => { beforeEach(() => { backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); initLogger({ - captureAttachments: true, + autoCaptureAttachments: true, projectId: "test-project-id", projectName: "groq.test.ts", }); @@ -434,7 +434,7 @@ describe("groq wrapper", () => { initLogger({ projectName: "groq.test.ts", projectId: "test-project-id", - captureAttachments, + autoCaptureAttachments: captureAttachments, }); const wrapped = wrapGroq({ audio: { diff --git a/js/tests/provider-wrappers.test.ts b/js/tests/provider-wrappers.test.ts index 17b807b69..ed7fb3552 100644 --- a/js/tests/provider-wrappers.test.ts +++ b/js/tests/provider-wrappers.test.ts @@ -76,8 +76,8 @@ describe("provider wrapper", () => { test("OpenAI responses image outputs are converted to attachments", async () => { const originalAutoCaptureAttachments = - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS = "true"; + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS = "true"; expect(await backgroundLogger.drain()).toHaveLength(0); const rawClient = new OpenAI({ apiKey: "sk-test" }); @@ -122,9 +122,9 @@ describe("provider wrapper", () => { } finally { client.responses.create = originalCreate; if (originalAutoCaptureAttachments === undefined) { - delete process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; + delete process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; } else { - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS = + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS = originalAutoCaptureAttachments; } } @@ -132,8 +132,8 @@ describe("provider wrapper", () => { test("OpenAI responses image data is omitted by default", async () => { const originalAutoCaptureAttachments = - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; - delete process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; + delete process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; const rawClient = new OpenAI({ apiKey: "sk-test" }); const client = wrapOpenAI(rawClient); const mockData = { @@ -163,9 +163,9 @@ describe("provider wrapper", () => { } finally { client.responses.create = originalCreate; if (originalAutoCaptureAttachments === undefined) { - delete process.env.BRAINTRUST_CAPTURE_ATTACHMENTS; + delete process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS; } else { - process.env.BRAINTRUST_CAPTURE_ATTACHMENTS = + process.env.BRAINTRUST_AUTO_CAPTURE_ATTACHMENTS = originalAutoCaptureAttachments; } } From f01391c7b005c9842e6ef38c24fcbf48cb1b735c Mon Sep 17 00:00:00 2001 From: lforst <8118419+lforst@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:25:11 +0000 Subject: [PATCH 18/19] chore: remove unused exports left by v4 merge --- js/src/edge-runtime-bootstrap.test.ts | 4 +- js/src/id-gen.ts | 11 -- js/src/instrumentation/core/tracing.ts | 9 -- js/src/instrumentation/core/types.ts | 42 +------ js/src/logger-caching.test.ts | 2 +- js/src/logger.ts | 149 ------------------------- js/src/logger/objects.ts | 2 +- js/src/logger/prompts.ts | 60 ++++------ js/src/prompt.test.ts | 5 +- knip.jsonc | 2 +- 10 files changed, 28 insertions(+), 258 deletions(-) delete mode 100644 js/src/logger.ts diff --git a/js/src/edge-runtime-bootstrap.test.ts b/js/src/edge-runtime-bootstrap.test.ts index d2982df20..47b6e2669 100644 --- a/js/src/edge-runtime-bootstrap.test.ts +++ b/js/src/edge-runtime-bootstrap.test.ts @@ -78,7 +78,7 @@ describe.each([ const platform = await import(entrypoint.replace("/index", "/config")); (platform.configureWorkerd ?? platform.configureEdgeLight)(); const braintrust = await import(entrypoint); - const { _exportsForTestingOnly } = await import("./logger"); + const { _exportsForTestingOnly } = await import("./logger/testing"); _exportsForTestingOnly.setInitialTestState(); await _exportsForTestingOnly.simulateLoginForTests(); @@ -126,7 +126,7 @@ describe.each([ const platform = await import(entrypoint.replace("/index", "/config")); (platform.configureWorkerd ?? platform.configureEdgeLight)(); const braintrust = await import(entrypoint); - const { _exportsForTestingOnly } = await import("./logger"); + const { _exportsForTestingOnly } = await import("./logger/testing"); _exportsForTestingOnly.setInitialTestState(); await _exportsForTestingOnly.simulateLoginForTests(); diff --git a/js/src/id-gen.ts b/js/src/id-gen.ts index 1d6f4a425..237dc0fec 100644 --- a/js/src/id-gen.ts +++ b/js/src/id-gen.ts @@ -101,17 +101,6 @@ function parseEnvBool(name: string): boolean { ); } -/** - * Whether OpenTelemetry compat mode is active, either via BRAINTRUST_OTEL_COMPAT - * or because `@braintrust/otel`'s `setupOtelCompat()` installed its globals. - */ -export function isOtelCompatMode(): boolean { - return ( - parseEnvBool("BRAINTRUST_OTEL_COMPAT") || - globalThis.BRAINTRUST_CONTEXT_MANAGER !== undefined - ); -} - let _warnedLegacyUuidConflict = false; /** diff --git a/js/src/instrumentation/core/tracing.ts b/js/src/instrumentation/core/tracing.ts index 871936f2b..e54d95f3d 100644 --- a/js/src/instrumentation/core/tracing.ts +++ b/js/src/instrumentation/core/tracing.ts @@ -781,12 +781,3 @@ export function traceSyncStreamCall( ); return result; } -export function unsubscribeAll( - unsubscribers: Array<() => void>, -): Array<() => void> { - for (const unsubscribe of unsubscribers) { - unsubscribe(); - } - - return []; -} diff --git a/js/src/instrumentation/core/types.ts b/js/src/instrumentation/core/types.ts index f6b2ab594..7b5e214e5 100644 --- a/js/src/instrumentation/core/types.ts +++ b/js/src/instrumentation/core/types.ts @@ -17,7 +17,7 @@ export type SpanInfoCarrier< /** * Per-call context passed to tracing callbacks. */ -export interface BaseContext { +interface BaseContext { /** * Unique identifier for this trace. */ @@ -39,37 +39,6 @@ export interface StartEvent extends BaseContext { arguments: TInput[]; } -/** - * Context containing a returned value. - */ -export interface EndEvent extends BaseContext { - /** - * The result of the synchronous portion. - * For async functions, this is the promise (not the resolved value). - */ - result: TResult; - - /** - * Arguments passed to the function (also available in StartEvent). - */ - arguments?: unknown[]; -} - -/** - * Context containing a thrown error or rejected promise. - */ -export interface ErrorEvent extends BaseContext { - /** - * The error that was thrown or the rejection reason. - */ - error: Error; - - /** - * Arguments passed to the function (also available in StartEvent). - */ - arguments?: unknown[]; -} - /** * Input context preserving the callable argument tuple. */ @@ -97,15 +66,6 @@ export interface TypedErrorEvent< arguments?: [...TArguments, ...any[]]; } -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface AsyncStartEvent extends StartEvent {} - -/** - * Context containing a resolved value for output extraction and finalization. - */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface AsyncEndEvent extends EndEvent {} - export type StartEventWith< TArguments extends EventArguments = unknown[], TExtra extends object = Record, diff --git a/js/src/logger-caching.test.ts b/js/src/logger-caching.test.ts index c7e4bc1e3..4377584f1 100644 --- a/js/src/logger-caching.test.ts +++ b/js/src/logger-caching.test.ts @@ -198,7 +198,7 @@ describe("project metadata caching", () => { "shares lookups across SDK copies and invalidates on %s", async (action) => { vi.resetModules(); - const otherSdk = await import("./logger"); + const otherSdk = await import("./logger/core"); const otherNode = await import("./node/config"); otherNode.configureNode(); diff --git a/js/src/logger.ts b/js/src/logger.ts deleted file mode 100644 index 4ae948ee8..000000000 --- a/js/src/logger.ts +++ /dev/null @@ -1,149 +0,0 @@ -// Compatibility barrel for source consumers. Public entrypoints import feature modules directly. -export { - LOGS3_OVERFLOW_REFERENCE_TYPE, - DEFAULT_MAX_REQUEST_SIZE, - ContextParentSpanIds, - LoginInvalidOrgError, - SetCurrentArg, - StartSpanArgs, - EndSpanArgs, - Exportable, - Span, - CurrentSpanStore, - ContextManager, - getContextManager, - configureContextManager, - NoopSpan, - NOOP_SPAN, - NOOP_SPAN_PERMALINK, - SerializedBraintrustState, - BraintrustState, - CAPTURE_ATTACHMENTS, - _internalCaptureAttachmentsEnabled, - _internalSetInitialState, - _internalGetGlobalState, - registerOtelFlush, - FailedHTTPResponse, - ObjectMetadata, - AttachmentParams, - ExternalAttachmentParams, - BaseAttachment, - Attachment, - ExternalAttachment, - ReadonlyAttachment, - JSONAttachment, - updateSpan, - _internalResumeSpan, - PropagationContext, - spanComponentsToObjectId, - ERR_PERMALINK, - permalink, - Logger, - Logs3OverflowUpload, - logs3OverflowUploadSchema, - Logs3OverflowInputRow, - constructLogs3OverflowRequest, - pickLogs3OverflowObjectIds, - uploadLogs3OverflowPayload, - utf8ByteLength, - BackgroundLoggerOpts, - TestBackgroundLogger, - InitLoggerOptions, - initLogger, - LoginOptions, - FullLoginOptions, - setMaskingFunction, - login, - loginToState, - log, - summarize, - currentExperiment, - currentLogger, - currentSpan, - getSpanParentObject, - _internalExportParentSynchronously, - _injectIntoCarrier, - injectTraceContext, - extractTraceContextFromHeaders, - logError, - traced, - wrapTraced, - startSpan, - _internalStartSpan, - _internalStartSpanWithInitialMerge, - _internalStartSpanWithInitialMergeAndParentSpanIds, - _internalStartSpanWithContext, - flush, - setFetch, - withCurrent, - withParent, - _internalWithParent, - deepCopyEvent, - BaseMetadata, - DefaultMetadataType, - EvalCaseOrigin, - EvalCase, - newId, - SpanImpl, -} from "./logger/core"; -export { - DatasetRestorePreviewResult, - DatasetRestoreResult, - DatasetSnapshotLookup, - DatasetRef, - InitDatasetOptions, - FullInitDatasetOptions, - initDataset, - Dataset, - AnyDataset, - DataSummary, - DatasetSummary, -} from "./logger/datasets"; -export { - ParametersRef, - InitOptions, - FullInitOptions, - init, - Experiment, - ReadonlyExperiment, - ScoreSummary, - MetricSummary, - ExperimentSummary, -} from "./logger/experiments"; -export { - LoadPromptOptions, - LoadParametersByIdOptions, - LoadParametersByIdWithEnvOptions, - LoadParametersByProjectNameOptions, - LoadParametersByProjectNameWithEnvOptions, - LoadParametersByProjectIdOptions, - LoadParametersByProjectIdWithEnvOptions, - LoadParametersOptions, - loadPrompt, - loadParameters, - CompiledPromptParams, - ChatPrompt, - CompletionPrompt, - CompiledPrompt, - DefaultPromptArgs, - renderMessage, - renderMessageImpl, - PromptRowWithId, - deserializePlainStringAsJSON, - renderPromptParams, - SerializedPrompt, - serializePromptForCache, - Prompt, - SerializedParameters, - serializeParametersForCache, - RemoteEvalParameters, - getPromptVersions, -} from "./logger/prompts"; -export { - WithTransactionId, - DEFAULT_FETCH_BATCH_SIZE, - MAX_BTQL_ITERATIONS, - ObjectFetcher, -} from "./logger/objects"; -export { _exportsForTestingOnly } from "./logger/testing"; -export type { DatasetSnapshot } from "./sdk-types"; diff --git a/js/src/logger/objects.ts b/js/src/logger/objects.ts index 0490146e9..76f7d4b94 100644 --- a/js/src/logger/objects.ts +++ b/js/src/logger/objects.ts @@ -8,7 +8,7 @@ export type WithTransactionId = R & { export const DEFAULT_FETCH_BATCH_SIZE = 1000; -export const MAX_BTQL_ITERATIONS = 10000; +const MAX_BTQL_ITERATIONS = 10000; export class ObjectFetcher implements AsyncIterable< WithTransactionId diff --git a/js/src/logger/prompts.ts b/js/src/logger/prompts.ts index 6adddbb20..3a1a77d3c 100644 --- a/js/src/logger/prompts.ts +++ b/js/src/logger/prompts.ts @@ -98,7 +98,7 @@ export type ParametersRow = { metadata?: Record | null; }; -export type LoadPromptOptions = FullLoginOptions & { +type LoadPromptOptions = FullLoginOptions & { projectName?: string; projectId?: string; slug?: string; @@ -114,52 +114,42 @@ type LoadParametersBaseOptions = FullLoginOptions & { state?: BraintrustState; }; -export type LoadParametersByIdOptions = LoadParametersBaseOptions & { +type LoadParametersByIdOptions = LoadParametersBaseOptions & { id: string; version?: string; }; -export type LoadParametersByIdWithEnvOptions = LoadParametersBaseOptions & { +type LoadParametersByIdWithEnvOptions = LoadParametersBaseOptions & { id: string; environment: string; version?: string; }; -export type LoadParametersByProjectNameOptions = LoadParametersBaseOptions & { +type LoadParametersByProjectNameOptions = LoadParametersBaseOptions & { projectName: string; slug: string; version?: string; }; -export type LoadParametersByProjectNameWithEnvOptions = - LoadParametersBaseOptions & { - projectName: string; - slug: string; - environment: string; - version?: string; - }; +type LoadParametersByProjectNameWithEnvOptions = LoadParametersBaseOptions & { + projectName: string; + slug: string; + environment: string; + version?: string; +}; -export type LoadParametersByProjectIdOptions = LoadParametersBaseOptions & { +type LoadParametersByProjectIdOptions = LoadParametersBaseOptions & { projectId: string; slug: string; version?: string; }; -export type LoadParametersByProjectIdWithEnvOptions = - LoadParametersBaseOptions & { - projectId: string; - slug: string; - environment: string; - version?: string; - }; - -export type LoadParametersOptions = - | LoadParametersByIdOptions - | LoadParametersByIdWithEnvOptions - | LoadParametersByProjectNameOptions - | LoadParametersByProjectNameWithEnvOptions - | LoadParametersByProjectIdOptions - | LoadParametersByProjectIdWithEnvOptions; +type LoadParametersByProjectIdWithEnvOptions = LoadParametersBaseOptions & { + projectId: string; + slug: string; + environment: string; + version?: string; +}; type LoadParametersImplementationOptions = LoadParametersBaseOptions & { projectName?: string; @@ -694,18 +684,6 @@ function expandAttachmentArrayPreTemplate( })); } -export function renderMessage( - render: (template: string) => string, - message: T, -): T; - -export function renderMessage( - render: (template: string) => string, - message: T, -): T { - return renderMessageImpl(render, message, {}); -} - export function renderMessageImpl( render: (template: string) => string, message: T, @@ -829,7 +807,7 @@ export type PromptRowWithId< (HasId extends true ? { id: string } : { id?: string }) & (HasVersion extends true ? { _xact_id: string } : { _xact_id?: string }); -export function deserializePlainStringAsJSON( +function deserializePlainStringAsJSON( s: string, ): { value: any; error: undefined } | { value: string; error: unknown } { if (s.trim() === "") { @@ -871,7 +849,7 @@ function renderTemplatedObject( return obj; } -export function renderPromptParams( +function renderPromptParams( params: ModelParams | undefined, args: Record, options: { strict?: boolean; templateFormat?: TemplateFormat } = {}, diff --git a/js/src/prompt.test.ts b/js/src/prompt.test.ts index 8530c5b9b..e121c042f 100644 --- a/js/src/prompt.test.ts +++ b/js/src/prompt.test.ts @@ -270,7 +270,7 @@ describe("prompt template_format", () => { describe("prompt template_format (unconfigured/browser-like)", () => { test("throws unsupported error for nunjucks template_format when not configured", async () => { vi.resetModules(); - const { Prompt: UnconfiguredPrompt } = await import("./logger"); + const { Prompt: UnconfiguredPrompt } = await import("./logger/prompts"); const prompt = new UnconfiguredPrompt( { @@ -308,7 +308,8 @@ describe("prompt template_format (unconfigured/browser-like)", () => { test("throws unsupported error after configureBrowser()", async () => { vi.resetModules(); const { configureBrowser } = await import("./browser/config"); - const { Prompt: BrowserConfiguredPrompt } = await import("./logger"); + const { Prompt: BrowserConfiguredPrompt } = + await import("./logger/prompts"); configureBrowser(); diff --git a/knip.jsonc b/knip.jsonc index 71c09b740..a608e88cb 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -48,7 +48,7 @@ "src/node/index.ts", "src/exports.ts", "src/auto.ts", - "src/auto-instrumentations/{index.ts,hook.mts,loader/esm-hook.mts}", + "src/auto-instrumentations/{index.ts,hook.mts,loader/module-hooks.mts}", "src/{node,non-node}/apply-auto-instrumentation-entry.ts", "src/{node,browser,workerd,edge-light}/{isomorph,config}.ts", "src/{custom-views,preprocessors}/exports.ts", From 0d21cc3f85301bfdde1c6a1b385e5b822ec95f8b Mon Sep 17 00:00:00 2001 From: lforst <8118419+lforst@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:05:56 +0000 Subject: [PATCH 19/19] fix(ci): align audio tests with v4 Node versions --- .github/workflows/checks.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 068647020..91d746e97 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -419,9 +419,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20, 22, 24, 26] - env: - ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: ${{ matrix.node-version == 20 && 'true' || '' }} + node-version: [22.18.0, 24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0