From 953f7ec94a44a82ab3719ab81b7cd6aeb2dad1de Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 30 Oct 2026 11:17:18 +0100 Subject: [PATCH 1/2] test(node-integration-tests): Install Flue and pi-durable as package dependencies The Flue and pi-durable suites installed their packages with an unlocked `npm install` into a fresh tmp dir for every `createEsmAndCjsTests` call (3 for Flue, 9 for pi-durable, per run and Node version). As root devDependencies they are installed once and pinned by the lockfile. The `engines` reason for keeping them out no longer applies: pnpm only warns about it. Both suites are ESM-only, so they now use `createEsmTests` instead of skipping the CJS mode by hand. Co-Authored-By: Claude Opus 5.5 --- .../node-integration-tests/package.json | 5 + .../suites/tracing/flue/test.ts | 319 ++-- .../suites/tracing/pi-durable/test.ts | 1282 ++++++++--------- pnpm-lock.yaml | 624 +++++++- pnpm-workspace.yaml | 2 + 5 files changed, 1302 insertions(+), 930 deletions(-) diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 563b661fed20..2965f6567c76 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -36,6 +36,10 @@ "@aws-sdk/client-sfn": "^3.1046.0", "@aws-sdk/client-sns": "^3.1046.0", "@aws-sdk/client-sqs": "^3.1046.0", + "@earendil-works/chord": "^1.4.0", + "@earendil-works/pi-ai": "^1.4.0", + "@earendil-works/pi-durable": "^1.4.0", + "@flue/runtime": "2.0.3", "@google/genai": "^1.20.0", "@growthbook/growthbook": "^1.6.5", "@hapi/hapi": "^21.3.10", @@ -113,6 +117,7 @@ "srvx": "^0.11.22", "tedious": "^19.2.1", "unstorage": "2.0.0-alpha.7", + "valibot": "1.4.2", "winston": "^3.17.0", "ws": "^8.20.1", "yargs": "^16.2.0" diff --git a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts index b750ee653e4f..ed1365a7ec4a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts @@ -17,195 +17,150 @@ import { } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { supports } from '../../../utils'; -import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; - -// `@flue/runtime` declares `engines.node >= 22.19`, so it can't live in the package's root -// `devDependencies` (that would break `yarn install` on the 20.19 CI matrix). Install it per-suite -// instead, guarded by the `min: 22` skip below. -const FLUE_DEPENDENCIES = { - additionalDependencies: { - '@flue/runtime': '2.0.3', - '@earendil-works/pi-ai': '0.85.1', - valibot: '1.1.0', - }, -}; +import { cleanupChildProcesses, createEsmTests } from '../../../utils/runner'; +// `@flue/runtime` declares `engines.node >= 22.19`. describe.runIf(supports({ min: 22 }))('Flue integration', () => { afterAll(() => { cleanupChildProcesses(); }); - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - // `@flue/runtime` is ESM-only — its `exports` map has no `require` condition, so there is no - // CJS variant of this scenario to run. - if (mode === 'cjs') { - return; - } - - test('creates the invoke_agent / chat / execute_tool hierarchy', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - const root = spans.find(span => span.name === 'flue-test')!; - expect(root.is_segment).toBe(true); - - // Counted rather than looked up: the interceptor skips the submission wrapper - // operation, so one dispatch opens exactly one agent span, and each turn and tool call - // is spanned once. `find` passes just as happily on a duplicate. - expect(spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.ai.flue')).toHaveLength(4); - - const agents = spans.filter(span => span.name === 'invoke_agent Hello'); - const chats = spans.filter(span => span.name === 'chat faux-model'); - const tools = spans.filter(span => span.name === 'execute_tool get_weather'); - expect(agents).toHaveLength(1); - expect(tools).toHaveLength(1); - // One turn asks for the tool, the second answers with its result. - expect(chats).toHaveLength(2); - - const agent = agents[0]!; - expect(agent.attributes['sentry.op']?.value).toBe('gen_ai.invoke_agent'); - expect(agent.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); - expect(agent.attributes[GEN_AI_AGENT_NAME]?.value).toBe('Hello'); - expect(agent.parent_span_id).toBe(root.span_id); - - const conversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; - expect(conversationId).toEqual(expect.any(String)); - - // Both turns, not just the first: they leave the provider by different paths (a tool - // call, then a final answer) and resolve their parent through separate tracker lookups. - for (const chat of chats) { - expect(chat.attributes['sentry.op']?.value).toBe('gen_ai.chat'); - expect(chat.attributes['sentry.origin']?.value).toBe('auto.ai.flue'); - expect(chat.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(chat.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); - expect(chat.parent_span_id).toBe(agent.span_id); - expect(chat.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBeGreaterThan(0); - expect(chat.attributes[GEN_AI_USAGE_OUTPUT_TOKENS]?.value).toBeGreaterThan(0); - expect(chat.attributes[GEN_AI_USAGE_TOTAL_TOKENS]?.value).toBeGreaterThan(0); - // Flue computes cost itself; no provider SDK reports it. The faux provider prices - // every model at zero, so this only proves the attribute is mapped. - expect(chat.attributes[GEN_AI_COST_TOTAL_TOKENS]?.value).toEqual(expect.any(Number)); - } - - // pi-ai's `toolUse` is reported as the conventions' `tool_call`. - expect(chats.map(chat => chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).sort()).toEqual([ - '["stop"]', - '["tool_call"]', - ]); - - const tool = tools[0]!; - // The tool has to actually run: a schema mismatch still produces a correctly named and - // parented span, so only the status separates a real call from a rejected one. - expect(tool.status).toBe('ok'); - expect(tool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); - expect(tool.attributes['sentry.origin']?.value).toBe('auto.ai.flue'); - expect(tool.attributes[GEN_AI_TOOL_NAME]?.value).toBe('get_weather'); - - // Tool spans are siblings of `chat` under the agent invocation, matching how Flue's - // own OpenTelemetry adapter projects them. - expect(tool.parent_span_id).toBe(agent.span_id); - }, - }) - .start() - .completed(); - }); - }, - FLUE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument-with-pii.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('records the messages of the turns in the gen_ai conventions shape', async () => { - await createRunner() - .expect({ - span: container => { - const chats = container.items - .filter(span => span.name === 'chat faux-model') - .sort((a, b) => a.start_timestamp - b.start_timestamp); - expect(chats).toHaveLength(2); - - // Flue sends pi-ai's shape (`toolResult`, `toolCall`); Sentry renders the conventions. - expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))).toEqual([ - { role: 'user', parts: [{ type: 'text', content: 'What is the weather in Berlin?' }] }, - ]); - expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_OUTPUT_MESSAGES]?.value))).toEqual([ - { - role: 'assistant', - parts: [{ type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Berlin"}' }], - finish_reason: 'tool_call', - }, - ]); - - const answerInput = JSON.parse(String(chats[1]!.attributes[GEN_AI_INPUT_MESSAGES]?.value)); - expect(answerInput.map((message: { role: string }) => message.role)).toEqual([ - 'user', - 'assistant', - 'tool', - ]); - expect(answerInput[2].parts[0]).toMatchObject({ - type: 'tool_call_response', - id: 'call_1', - name: 'get_weather', - }); - expect(answerInput[2].parts[0].result).toContain('sunny in Berlin'); - expect(chats[1]!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value).toContain('You are a helpful assistant.'); - - // The tool span records the result the model receives, not Flue's internal wrapper. - const tool = container.items.find(span => span.name === 'execute_tool get_weather')!; - expect(tool.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"city":"Berlin"}'); - expect(tool.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('sunny in Berlin'); - expect(tool.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).not.toContain('details'); - }, - }) - .start() - .completed(); - }); - }, - FLUE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-separate-requests.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('puts each dispatch in the trace of the request that admitted it', async () => { - const expectAgentUnder = (requestName: string) => ({ - span: (container: SerializedStreamedSpanContainer) => { - const request = container.items.find(span => span.name === requestName)!; - expect(request.is_segment).toBe(true); - - const agents = container.items.filter(span => span.name === 'invoke_agent Hello'); + // `@flue/runtime` is ESM-only: its `exports` map has no `require` condition. + createEsmTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('creates the invoke_agent / chat / execute_tool hierarchy', async () => { + await createRunner() + .expect({ + span: container => { + const spans = container.items; + + const root = spans.find(span => span.name === 'flue-test')!; + expect(root.is_segment).toBe(true); + + // Counted rather than looked up: the interceptor skips the submission wrapper + // operation, so one dispatch opens exactly one agent span, and each turn and tool call + // is spanned once. `find` passes just as happily on a duplicate. + expect(spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.ai.flue')).toHaveLength(4); + + const agents = spans.filter(span => span.name === 'invoke_agent Hello'); + const chats = spans.filter(span => span.name === 'chat faux-model'); + const tools = spans.filter(span => span.name === 'execute_tool get_weather'); expect(agents).toHaveLength(1); - expect(agents[0]!.parent_span_id).toBe(request.span_id); + expect(tools).toHaveLength(1); + // One turn asks for the tool, the second answers with its result. + expect(chats).toHaveLength(2); + + const agent = agents[0]!; + expect(agent.attributes['sentry.op']?.value).toBe('gen_ai.invoke_agent'); + expect(agent.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); + expect(agent.attributes[GEN_AI_AGENT_NAME]?.value).toBe('Hello'); + expect(agent.parent_span_id).toBe(root.span_id); + + const conversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; + expect(conversationId).toEqual(expect.any(String)); + + // Both turns, not just the first: they leave the provider by different paths (a tool + // call, then a final answer) and resolve their parent through separate tracker lookups. + for (const chat of chats) { + expect(chat.attributes['sentry.op']?.value).toBe('gen_ai.chat'); + expect(chat.attributes['sentry.origin']?.value).toBe('auto.ai.flue'); + expect(chat.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); + expect(chat.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); + expect(chat.parent_span_id).toBe(agent.span_id); + expect(chat.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBeGreaterThan(0); + expect(chat.attributes[GEN_AI_USAGE_OUTPUT_TOKENS]?.value).toBeGreaterThan(0); + expect(chat.attributes[GEN_AI_USAGE_TOTAL_TOKENS]?.value).toBeGreaterThan(0); + // Flue computes cost itself; no provider SDK reports it. The faux provider prices + // every model at zero, so this only proves the attribute is mapped. + expect(chat.attributes[GEN_AI_COST_TOTAL_TOKENS]?.value).toEqual(expect.any(Number)); + } + + // pi-ai's `toolUse` is reported as the conventions' `tool_call`. + expect(chats.map(chat => chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).sort()).toEqual([ + '["stop"]', + '["tool_call"]', + ]); + + const tool = tools[0]!; + // The tool has to actually run: a schema mismatch still produces a correctly named and + // parented span, so only the status separates a real call from a rejected one. + expect(tool.status).toBe('ok'); + expect(tool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); + expect(tool.attributes['sentry.origin']?.value).toBe('auto.ai.flue'); + expect(tool.attributes[GEN_AI_TOOL_NAME]?.value).toBe('get_weather'); + + // Tool spans are siblings of `chat` under the agent invocation, matching how Flue's + // own OpenTelemetry adapter projects them. + expect(tool.parent_span_id).toBe(agent.span_id); + }, + }) + .start() + .completed(); + }); + }); + + createEsmTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('records the messages of the turns in the gen_ai conventions shape', async () => { + await createRunner() + .expect({ + span: container => { + const chats = container.items + .filter(span => span.name === 'chat faux-model') + .sort((a, b) => a.start_timestamp - b.start_timestamp); + expect(chats).toHaveLength(2); + + // Flue sends pi-ai's shape (`toolResult`, `toolCall`); Sentry renders the conventions. + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))).toEqual([ + { role: 'user', parts: [{ type: 'text', content: 'What is the weather in Berlin?' }] }, + ]); + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_OUTPUT_MESSAGES]?.value))).toEqual([ + { + role: 'assistant', + parts: [{ type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Berlin"}' }], + finish_reason: 'tool_call', + }, + ]); + + const answerInput = JSON.parse(String(chats[1]!.attributes[GEN_AI_INPUT_MESSAGES]?.value)); + expect(answerInput.map((message: { role: string }) => message.role)).toEqual(['user', 'assistant', 'tool']); + expect(answerInput[2].parts[0]).toMatchObject({ + type: 'tool_call_response', + id: 'call_1', + name: 'get_weather', + }); + expect(answerInput[2].parts[0].result).toContain('sunny in Berlin'); + expect(chats[1]!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value).toContain('You are a helpful assistant.'); + + // The tool span records the result the model receives, not Flue's internal wrapper. + const tool = container.items.find(span => span.name === 'execute_tool get_weather')!; + expect(tool.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"city":"Berlin"}'); + expect(tool.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('sunny in Berlin'); + expect(tool.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).not.toContain('details'); }, - }); - - await createRunner() - .unordered() - .expect(expectAgentUnder('request-a')) - .expect(expectAgentUnder('request-b')) - .start() - .completed(); + }) + .start() + .completed(); + }); + }); + + createEsmTests(__dirname, 'scenario-separate-requests.mjs', 'instrument.mjs', (createRunner, test) => { + test('puts each dispatch in the trace of the request that admitted it', async () => { + const expectAgentUnder = (requestName: string) => ({ + span: (container: SerializedStreamedSpanContainer) => { + const request = container.items.find(span => span.name === requestName)!; + expect(request.is_segment).toBe(true); + + const agents = container.items.filter(span => span.name === 'invoke_agent Hello'); + expect(agents).toHaveLength(1); + expect(agents[0]!.parent_span_id).toBe(request.span_id); + }, }); - }, - FLUE_DEPENDENCIES, - ); + + await createRunner() + .unordered() + .expect(expectAgentUnder('request-a')) + .expect(expectAgentUnder('request-b')) + .start() + .completed(); + }); + }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/pi-durable/test.ts b/dev-packages/node-integration-tests/suites/tracing/pi-durable/test.ts index 7bfd7947bb9f..f26aa34b63d7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/pi-durable/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/pi-durable/test.ts @@ -23,18 +23,7 @@ import { GEN_AI_CHAT, GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/c import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { supports } from '../../../utils'; -import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; - -// The pi packages declare `engines.node >= 22.19`, so they can't live in the package's root -// `devDependencies` (that would break `yarn install` on the 20.19 CI matrix). Install them per-suite -// instead, guarded by the `min: 22` skip below. -const PI_DURABLE_DEPENDENCIES = { - additionalDependencies: { - '@earendil-works/pi-durable': '^1.2.0', - '@earendil-works/pi-ai': '^1.2.0', - '@earendil-works/chord': '^1.2.0', - }, -}; +import { cleanupChildProcesses, createEsmTests } from '../../../utils/runner'; // The provider session id that pi-durable stores with each conversation, a UUIDv7. const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; @@ -48,702 +37,613 @@ const CONTENT_ATTRIBUTES = [ GEN_AI_TOOL_CALL_RESULT, ]; +// The pi packages declare `engines.node >= 22.19`. describe.runIf(supports({ min: 22 }))('pi-durable integration', () => { afterAll(() => { cleanupChildProcesses(); }); - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - // The pi packages are ESM-only: their `exports` maps have no `require` condition. - if (mode === 'cjs') { - return; - } - - test('traces each run as its own invoke_agent trace with chat and execute_tool children', async () => { - let toolRunTraceId: string | undefined; - let brokenToolSpanId: string | undefined; - let errorTraceId: string | undefined; - let errorSpanId: string | undefined; - const conversationIds: unknown[] = []; - - await createRunner() - .unordered() - .expect({ - span: container => { - const spans = container.items; - const agent = spans.find(span => span.is_segment)!; - expect(agent.name).toBe('invoke_agent'); - expect(spans.filter(span => span.name === 'chat faux-model')).toHaveLength(2); - // One span per run, request and tool call: a tool wrapped twice would add a span. - expect(spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.pi_durable')).toHaveLength( - 5, - ); - - expect(agent.attributes[SENTRY_OP]?.value).toBe(GEN_AI_INVOKE_AGENT); - expect(agent.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.pi_durable'); - expect(agent.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); - const conversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; - expect(conversationId).toMatch(UUID); - - const chats = spans - .filter(span => span.name === 'chat faux-model') - .sort((a, b) => a.start_timestamp - b.start_timestamp); - for (const chat of chats) { - expect(chat.parent_span_id).toBe(agent.span_id); - expect(chat.attributes[SENTRY_OP]?.value).toBe(GEN_AI_CHAT); - expect(chat.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.pi_durable'); - expect(chat.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe('faux'); - expect(chat.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('faux-model'); - expect(chat.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); - expect(chat.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBeGreaterThan(0); - expect(chat.attributes[GEN_AI_USAGE_OUTPUT_TOKENS]?.value).toBeGreaterThan(0); - // pi-durable sends the prompt and the tools as positional system messages. - expect(chat.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value).toBe('You are a weather assistant.'); - expect( - JSON.parse(String(chat.attributes[GEN_AI_TOOL_DEFINITIONS]?.value)).map( - (tool: { name: string }) => tool.name, - ), - ).toEqual(['get_weather', 'broken']); - } - - // Mapped from pi-ai's shape to the conventions: the tool calls and tool results the - // second request carries are what Sentry's conversation view renders. - expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))).toEqual([ - { role: 'user', parts: [{ type: 'text', content: 'What is the weather in Berlin?' }] }, - ]); - const answerInput = JSON.parse(String(chats[1]!.attributes[GEN_AI_INPUT_MESSAGES]?.value)); - expect(answerInput.map((message: { role: string }) => message.role)).toEqual([ - 'user', - 'assistant', - 'tool', - 'tool', - ]); - expect(answerInput[1].parts).toEqual([ - { type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Berlin"}' }, - { type: 'tool_call', id: 'call_2', name: 'broken', arguments: '{}' }, - ]); - expect(answerInput[2].parts).toEqual([ - { - type: 'tool_call_response', - id: 'call_1', - name: 'get_weather', - result: 'It is 21 degrees and sunny in Berlin.', - }, - ]); - expect(chats.map(chat => chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).sort()).toEqual([ - '["stop"]', - '["tool_call"]', - ]); - expect(chats.map(chat => chat.attributes[GEN_AI_OUTPUT_MESSAGES]?.value).join()).toContain( - 'It is 21 degrees and sunny in Berlin.', - ); - - const weather = spans.find(span => span.name === 'execute_tool get_weather')!; - expect(weather.parent_span_id).toBe(agent.span_id); - expect(weather.status).toBe('ok'); - expect(weather.attributes[SENTRY_OP]?.value).toBe(GEN_AI_EXECUTE_TOOL); - expect(weather.attributes[GEN_AI_TOOL_NAME]?.value).toBe('get_weather'); - expect(weather.attributes[GEN_AI_TOOL_DESCRIPTION]?.value).toBe('Get the current weather for a city.'); - expect(weather.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]?.value).toBe('call_1'); - expect(weather.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"city":"Berlin"}'); - expect(weather.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('sunny in Berlin'); - expect(weather.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); - - const broken = spans.find(span => span.name === 'execute_tool broken')!; - expect(broken.parent_span_id).toBe(agent.span_id); - expect(broken.status).toBe('error'); - // The error result pi-durable gives the model, not the thrown error. - expect(broken.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('[error] broken tool'); - - toolRunTraceId = agent.trace_id; - brokenToolSpanId = broken.span_id; - conversationIds.push(conversationId); - }, - }) - .expect({ - span: container => { - const spans = container.items; - const agent = spans.find(span => span.is_segment)!; - expect(agent.name).toBe('invoke_agent'); - expect(spans).toHaveLength(2); - - const chat = spans.find(span => span.name === 'chat faux-model')!; + // The pi packages are ESM-only: their `exports` maps have no `require` condition. + createEsmTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('traces each run as its own invoke_agent trace with chat and execute_tool children', async () => { + let toolRunTraceId: string | undefined; + let brokenToolSpanId: string | undefined; + let errorTraceId: string | undefined; + let errorSpanId: string | undefined; + const conversationIds: unknown[] = []; + + await createRunner() + .unordered() + .expect({ + span: container => { + const spans = container.items; + const agent = spans.find(span => span.is_segment)!; + expect(agent.name).toBe('invoke_agent'); + expect(spans.filter(span => span.name === 'chat faux-model')).toHaveLength(2); + // One span per run, request and tool call: a tool wrapped twice would add a span. + expect(spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.pi_durable')).toHaveLength( + 5, + ); + + expect(agent.attributes[SENTRY_OP]?.value).toBe(GEN_AI_INVOKE_AGENT); + expect(agent.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.pi_durable'); + expect(agent.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); + const conversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; + expect(conversationId).toMatch(UUID); + + const chats = spans + .filter(span => span.name === 'chat faux-model') + .sort((a, b) => a.start_timestamp - b.start_timestamp); + for (const chat of chats) { expect(chat.parent_span_id).toBe(agent.span_id); - expect(chat.attributes[GEN_AI_OUTPUT_MESSAGES]?.value).toContain('Goodbye.'); - - conversationIds.push(agent.attributes[GEN_AI_CONVERSATION_ID]?.value); - }, - }) - .expect({ - span: container => { - // The request that opened the Harness and submitted both inputs holds no agent work. - expect(container.items).toHaveLength(1); - expect(container.items[0]!.name).toBe('pi-durable-request'); - }, - }) - .expect({ - event: event => { - const exception = event.exception?.values?.[0]; - expect(exception?.value).toBe('broken tool'); - expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); - - errorTraceId = event.contexts?.trace?.trace_id; - errorSpanId = event.contexts?.trace?.span_id; - }, - }) - .start() - .completed(); - - // Both runs belong to the root conversation, so they share one conversation id. - expect(conversationIds).toHaveLength(2); - expect(conversationIds[0]).toBe(conversationIds[1]); - // The tool error is reported on the failing call's span, in the run's trace. - expect(errorTraceId).toBe(toolRunTraceId); - expect(errorSpanId).toBe(brokenToolSpanId); - }); - - test('records no message content when the client turns gen_ai recording off', async () => { - await createRunner() - .withEnv({ PI_DURABLE_RECORDING: 'off' }) - .unordered() - .expect({ - span: container => { - const spans = container.items; - expect(spans).toHaveLength(5); - for (const span of spans) { - for (const attribute of CONTENT_ATTRIBUTES) { - expect(span.attributes[attribute]).toBeUndefined(); - } - } - const weather = spans.find(span => span.name === 'execute_tool get_weather')!; - expect(weather.attributes[GEN_AI_TOOL_DESCRIPTION]?.value).toBe('Get the current weather for a city.'); + expect(chat.attributes[SENTRY_OP]?.value).toBe(GEN_AI_CHAT); + expect(chat.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.pi_durable'); + expect(chat.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe('faux'); + expect(chat.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('faux-model'); + expect(chat.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); + expect(chat.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBeGreaterThan(0); + expect(chat.attributes[GEN_AI_USAGE_OUTPUT_TOKENS]?.value).toBeGreaterThan(0); + // pi-durable sends the prompt and the tools as positional system messages. + expect(chat.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value).toBe('You are a weather assistant.'); expect( - spans.find(span => span.name === 'chat faux-model')!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value, - ).toBeGreaterThan(0); - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - for (const span of container.items) { - for (const attribute of CONTENT_ATTRIBUTES) { - expect(span.attributes[attribute]).toBeUndefined(); - } + JSON.parse(String(chat.attributes[GEN_AI_TOOL_DEFINITIONS]?.value)).map( + (tool: { name: string }) => tool.name, + ), + ).toEqual(['get_weather', 'broken']); + } + + // Mapped from pi-ai's shape to the conventions: the tool calls and tool results the + // second request carries are what Sentry's conversation view renders. + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))).toEqual([ + { role: 'user', parts: [{ type: 'text', content: 'What is the weather in Berlin?' }] }, + ]); + const answerInput = JSON.parse(String(chats[1]!.attributes[GEN_AI_INPUT_MESSAGES]?.value)); + expect(answerInput.map((message: { role: string }) => message.role)).toEqual([ + 'user', + 'assistant', + 'tool', + 'tool', + ]); + expect(answerInput[1].parts).toEqual([ + { type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Berlin"}' }, + { type: 'tool_call', id: 'call_2', name: 'broken', arguments: '{}' }, + ]); + expect(answerInput[2].parts).toEqual([ + { + type: 'tool_call_response', + id: 'call_1', + name: 'get_weather', + result: 'It is 21 degrees and sunny in Berlin.', + }, + ]); + expect(chats.map(chat => chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).sort()).toEqual([ + '["stop"]', + '["tool_call"]', + ]); + expect(chats.map(chat => chat.attributes[GEN_AI_OUTPUT_MESSAGES]?.value).join()).toContain( + 'It is 21 degrees and sunny in Berlin.', + ); + + const weather = spans.find(span => span.name === 'execute_tool get_weather')!; + expect(weather.parent_span_id).toBe(agent.span_id); + expect(weather.status).toBe('ok'); + expect(weather.attributes[SENTRY_OP]?.value).toBe(GEN_AI_EXECUTE_TOOL); + expect(weather.attributes[GEN_AI_TOOL_NAME]?.value).toBe('get_weather'); + expect(weather.attributes[GEN_AI_TOOL_DESCRIPTION]?.value).toBe('Get the current weather for a city.'); + expect(weather.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]?.value).toBe('call_1'); + expect(weather.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"city":"Berlin"}'); + expect(weather.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('sunny in Berlin'); + expect(weather.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); + + const broken = spans.find(span => span.name === 'execute_tool broken')!; + expect(broken.parent_span_id).toBe(agent.span_id); + expect(broken.status).toBe('error'); + // The error result pi-durable gives the model, not the thrown error. + expect(broken.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('[error] broken tool'); + + toolRunTraceId = agent.trace_id; + brokenToolSpanId = broken.span_id; + conversationIds.push(conversationId); + }, + }) + .expect({ + span: container => { + const spans = container.items; + const agent = spans.find(span => span.is_segment)!; + expect(agent.name).toBe('invoke_agent'); + expect(spans).toHaveLength(2); + + const chat = spans.find(span => span.name === 'chat faux-model')!; + expect(chat.parent_span_id).toBe(agent.span_id); + expect(chat.attributes[GEN_AI_OUTPUT_MESSAGES]?.value).toContain('Goodbye.'); + + conversationIds.push(agent.attributes[GEN_AI_CONVERSATION_ID]?.value); + }, + }) + .expect({ + span: container => { + // The request that opened the Harness and submitted both inputs holds no agent work. + expect(container.items).toHaveLength(1); + expect(container.items[0]!.name).toBe('pi-durable-request'); + }, + }) + .expect({ + event: event => { + const exception = event.exception?.values?.[0]; + expect(exception?.value).toBe('broken tool'); + expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); + + errorTraceId = event.contexts?.trace?.trace_id; + errorSpanId = event.contexts?.trace?.span_id; + }, + }) + .start() + .completed(); + + // Both runs belong to the root conversation, so they share one conversation id. + expect(conversationIds).toHaveLength(2); + expect(conversationIds[0]).toBe(conversationIds[1]); + // The tool error is reported on the failing call's span, in the run's trace. + expect(errorTraceId).toBe(toolRunTraceId); + expect(errorSpanId).toBe(brokenToolSpanId); + }); + + test('records no message content when the client turns gen_ai recording off', async () => { + await createRunner() + .withEnv({ PI_DURABLE_RECORDING: 'off' }) + .unordered() + .expect({ + span: container => { + const spans = container.items; + expect(spans).toHaveLength(5); + for (const span of spans) { + for (const attribute of CONTENT_ATTRIBUTES) { + expect(span.attributes[attribute]).toBeUndefined(); } - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(1); - expect(container.items[0]!.name).toBe('pi-durable-request'); - }, - }) - .expect({ - event: event => { - expect(event.exception?.values?.[0]?.value).toBe('broken tool'); - }, - }) - .start() - .completed(); - }); - }, - PI_DURABLE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-tools.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('reports app tool failures but not failures of the built-in coding tools', async () => { - const appErrors: unknown[] = []; - - // Ordered, with spans ignored: a captured failure of the built-in `bash` tool would be an - // extra event before the sentinel, which the scenario sends once everything else is sent. - // The two app failures are processed concurrently, so they may arrive in either order. - await createRunner() - .ignore('span') - .expect({ - event: event => { - expect(['app read failed', 'Intentional pi-durable tool failure']).toContain( - event.exception?.values?.[0]?.value, - ); - appErrors.push(event.exception?.values?.[0]?.value); - }, - }) - .expect({ - event: event => { - expect(['app read failed', 'Intentional pi-durable tool failure']).toContain( - event.exception?.values?.[0]?.value, - ); - appErrors.push(event.exception?.values?.[0]?.value); - }, - }) - .expect({ - event: event => { - expect(event.message).toBe('pi-durable tools done'); - }, - }) - .start() - .completed(); - - expect(appErrors.sort()).toEqual(['Intentional pi-durable tool failure', 'app read failed']); - }); - - test('records the tool result the model receives', async () => { - let errorSpanId: string | undefined; - let failingToolSpanId: string | undefined; + } + const weather = spans.find(span => span.name === 'execute_tool get_weather')!; + expect(weather.attributes[GEN_AI_TOOL_DESCRIPTION]?.value).toBe('Get the current weather for a city.'); + expect( + spans.find(span => span.name === 'chat faux-model')!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value, + ).toBeGreaterThan(0); + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + for (const span of container.items) { + for (const attribute of CONTENT_ATTRIBUTES) { + expect(span.attributes[attribute]).toBeUndefined(); + } + } + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + expect(container.items[0]!.name).toBe('pi-durable-request'); + }, + }) + .expect({ + event: event => { + expect(event.exception?.values?.[0]?.value).toBe('broken tool'); + }, + }) + .start() + .completed(); + }); + }); - await createRunner() - .unordered() - .expect({ - event: event => { - const exception = event.exception?.values?.[0]; - expect(exception?.value).toBe('Intentional pi-durable tool failure'); - expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); - errorSpanId = event.contexts?.trace?.span_id; - }, - }) - .expect({ - event: event => { - expect(event.exception?.values?.[0]?.value).toBe('app read failed'); - }, - }) - .expect({ - span: container => { - const spans = container.items; - - const bash = spans.find(span => span.name === 'execute_tool bash')!; - expect(bash.status).toBe('error'); - expect(bash.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toMatch(/Command exited with code \d+/); - - const read = spans.find(span => span.name === 'execute_tool read')!; - expect(read.status).toBe('error'); - expect(read.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('app read failed'); - - // The streamed output becomes the result, since the tool returns no content. - const streamer = spans.find(span => span.name === 'execute_tool streamer')!; - expect(streamer.status).toBe('ok'); - expect(streamer.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('line 1\nline 2\n'); - - const secret = spans.find(span => span.name === 'execute_tool secret')!; - expect(secret.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('redacted by afterTool'); - - const failNow = spans.find(span => span.name === 'execute_tool fail_now')!; - expect(failNow.status).toBe('error'); - failingToolSpanId = failNow.span_id; - }, - }) - .start() - .completed(); + createEsmTests(__dirname, 'scenario-tools.mjs', 'instrument.mjs', (createRunner, test) => { + test('reports app tool failures but not failures of the built-in coding tools', async () => { + const appErrors: unknown[] = []; + + // Ordered, with spans ignored: a captured failure of the built-in `bash` tool would be an + // extra event before the sentinel, which the scenario sends once everything else is sent. + // The two app failures are processed concurrently, so they may arrive in either order. + await createRunner() + .ignore('span') + .expect({ + event: event => { + expect(['app read failed', 'Intentional pi-durable tool failure']).toContain( + event.exception?.values?.[0]?.value, + ); + appErrors.push(event.exception?.values?.[0]?.value); + }, + }) + .expect({ + event: event => { + expect(['app read failed', 'Intentional pi-durable tool failure']).toContain( + event.exception?.values?.[0]?.value, + ); + appErrors.push(event.exception?.values?.[0]?.value); + }, + }) + .expect({ + event: event => { + expect(event.message).toBe('pi-durable tools done'); + }, + }) + .start() + .completed(); + + expect(appErrors.sort()).toEqual(['Intentional pi-durable tool failure', 'app read failed']); + }); + + test('records the tool result the model receives', async () => { + let errorSpanId: string | undefined; + let failingToolSpanId: string | undefined; + + await createRunner() + .unordered() + .expect({ + event: event => { + const exception = event.exception?.values?.[0]; + expect(exception?.value).toBe('Intentional pi-durable tool failure'); + expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); + errorSpanId = event.contexts?.trace?.span_id; + }, + }) + .expect({ + event: event => { + expect(event.exception?.values?.[0]?.value).toBe('app read failed'); + }, + }) + .expect({ + span: container => { + const spans = container.items; + + const bash = spans.find(span => span.name === 'execute_tool bash')!; + expect(bash.status).toBe('error'); + expect(bash.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toMatch(/Command exited with code \d+/); + + const read = spans.find(span => span.name === 'execute_tool read')!; + expect(read.status).toBe('error'); + expect(read.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('app read failed'); + + // The streamed output becomes the result, since the tool returns no content. + const streamer = spans.find(span => span.name === 'execute_tool streamer')!; + expect(streamer.status).toBe('ok'); + expect(streamer.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('line 1\nline 2\n'); + + const secret = spans.find(span => span.name === 'execute_tool secret')!; + expect(secret.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('redacted by afterTool'); + + const failNow = spans.find(span => span.name === 'execute_tool fail_now')!; + expect(failNow.status).toBe('error'); + failingToolSpanId = failNow.span_id; + }, + }) + .start() + .completed(); + + expect(errorSpanId).toBe(failingToolSpanId); + }); + }); - expect(errorSpanId).toBe(failingToolSpanId); - }); - }, - PI_DURABLE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-reports.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('captures failures pi-durable does not propagate, and tags idle compaction with its conversation', async () => { - let runConversationId: unknown; - let compactionConversationId: unknown; + createEsmTests(__dirname, 'scenario-reports.mjs', 'instrument.mjs', (createRunner, test) => { + test('captures failures pi-durable does not propagate, and tags idle compaction with its conversation', async () => { + let runConversationId: unknown; + let compactionConversationId: unknown; + + await createRunner() + .unordered() + .expect({ + event: event => { + const exception = event.exception?.values?.[0]; + expect(exception?.value).toBe('afterResponse hook failed'); + expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); + }, + }) + .expect({ + event: event => { + const exception = event.exception?.values?.[0]; + expect(exception?.value).toBe('card declined'); + expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); + }, + }) + .expect({ + span: container => { + const agent = container.items.find(span => span.is_segment)!; + expect(agent.name).toBe('invoke_agent'); + runConversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; + }, + }) + .expect({ + span: container => { + // A compaction started while idle belongs to no run, so its request is a trace of its own. + expect(container.items).toHaveLength(1); + const summary = container.items[0]!; + expect(summary.is_segment).toBe(true); + expect(summary.attributes[SENTRY_OP]?.value).toBe(GEN_AI_CHAT); + compactionConversationId = summary.attributes[GEN_AI_CONVERSATION_ID]?.value; + }, + }) + .start() + .completed(); + + expect(runConversationId).toMatch(UUID); + expect(compactionConversationId).toBe(runConversationId); + }); + }); - await createRunner() - .unordered() - .expect({ - event: event => { - const exception = event.exception?.values?.[0]; - expect(exception?.value).toBe('afterResponse hook failed'); - expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); - }, - }) - .expect({ - event: event => { - const exception = event.exception?.values?.[0]; - expect(exception?.value).toBe('card declined'); - expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); - }, - }) - .expect({ - span: container => { - const agent = container.items.find(span => span.is_segment)!; - expect(agent.name).toBe('invoke_agent'); - runConversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; - }, - }) - .expect({ - span: container => { - // A compaction started while idle belongs to no run, so its request is a trace of its own. - expect(container.items).toHaveLength(1); - const summary = container.items[0]!; - expect(summary.is_segment).toBe(true); - expect(summary.attributes[SENTRY_OP]?.value).toBe(GEN_AI_CHAT); - compactionConversationId = summary.attributes[GEN_AI_CONVERSATION_ID]?.value; - }, - }) - .start() - .completed(); + createEsmTests(__dirname, 'scenario-subagent.mjs', 'instrument.mjs', (createRunner, test) => { + test('nests subagent runs under the tool call that started them', async () => { + await createRunner() + .unordered() + .expect({ + span: container => { + const spans = container.items; + const parent = spans.find(span => span.is_segment)!; + const delegate = spans.find(span => span.name === 'execute_tool delegate')!; + expect(parent.name).toBe('invoke_agent'); + expect(delegate.parent_span_id).toBe(parent.span_id); + + // The subagent's run is a child of the delegating call, in the same trace, on its own + // conversation. + const child = spans.find(span => span.name === 'invoke_agent' && !span.is_segment)!; + expect(child.parent_span_id).toBe(delegate.span_id); + const parentConversation = String(parent.attributes[GEN_AI_CONVERSATION_ID]?.value); + const childConversation = String(child.attributes[GEN_AI_CONVERSATION_ID]?.value); + expect(parentConversation).toMatch(UUID); + expect(childConversation).toMatch(UUID); + expect(childConversation).not.toBe(parentConversation); + + const childChat = spans.find(span => span.parent_span_id === child.span_id)!; + expect(childChat.name).toBe('chat faux-model'); + expect(childChat.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(childConversation); + expect(delegate.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('Answer: Weather in Vienna?'); + }, + }) + .expect({ + span: container => { + const spans = container.items; + const parent = spans.find(span => span.is_segment)!; + const fork = spans.find(span => span.name === 'execute_tool fork_delegate')!; + const child = spans.find(span => span.name === 'invoke_agent' && !span.is_segment)!; + expect(fork.parent_span_id).toBe(parent.span_id); + expect(child.parent_span_id).toBe(fork.span_id); + expect(fork.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('Answer: Forked task.'); + }, + }) + .expect({ + span: container => { + const spans = container.items; + const batch = spans.find(span => span.name === 'execute_tool batch')!; + const inner = spans.find(span => span.name === 'execute_tool inner')!; + expect(batch.parent_span_id).toBe(spans.find(span => span.is_segment)!.span_id); + // A tool that runs another tool itself: the inner call ends with its own result. + expect(inner.parent_span_id).toBe(batch.span_id); + expect(inner.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('INNER RESULT'); + expect(batch.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('OUTER RESULT'); + }, + }) + .start() + .completed(); + }); + }); - expect(runConversationId).toMatch(UUID); - expect(compactionConversationId).toBe(runConversationId); - }); - }, - PI_DURABLE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-subagent.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('nests subagent runs under the tool call that started them', async () => { - await createRunner() - .unordered() - .expect({ - span: container => { - const spans = container.items; - const parent = spans.find(span => span.is_segment)!; - const delegate = spans.find(span => span.name === 'execute_tool delegate')!; - expect(parent.name).toBe('invoke_agent'); - expect(delegate.parent_span_id).toBe(parent.span_id); - - // The subagent's run is a child of the delegating call, in the same trace, on its own - // conversation. - const child = spans.find(span => span.name === 'invoke_agent' && !span.is_segment)!; - expect(child.parent_span_id).toBe(delegate.span_id); - const parentConversation = String(parent.attributes[GEN_AI_CONVERSATION_ID]?.value); - const childConversation = String(child.attributes[GEN_AI_CONVERSATION_ID]?.value); - expect(parentConversation).toMatch(UUID); - expect(childConversation).toMatch(UUID); - expect(childConversation).not.toBe(parentConversation); - - const childChat = spans.find(span => span.parent_span_id === child.span_id)!; - expect(childChat.name).toBe('chat faux-model'); - expect(childChat.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(childConversation); - expect(delegate.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('Answer: Weather in Vienna?'); - }, - }) - .expect({ - span: container => { - const spans = container.items; - const parent = spans.find(span => span.is_segment)!; - const fork = spans.find(span => span.name === 'execute_tool fork_delegate')!; - const child = spans.find(span => span.name === 'invoke_agent' && !span.is_segment)!; - expect(fork.parent_span_id).toBe(parent.span_id); - expect(child.parent_span_id).toBe(fork.span_id); - expect(fork.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('Answer: Forked task.'); - }, - }) - .expect({ - span: container => { - const spans = container.items; - const batch = spans.find(span => span.name === 'execute_tool batch')!; - const inner = spans.find(span => span.name === 'execute_tool inner')!; - expect(batch.parent_span_id).toBe(spans.find(span => span.is_segment)!.span_id); - // A tool that runs another tool itself: the inner call ends with its own result. - expect(inner.parent_span_id).toBe(batch.span_id); - expect(inner.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('INNER RESULT'); - expect(batch.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toBe('OUTER RESULT'); - }, - }) - .start() - .completed(); - }); - }, - PI_DURABLE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-retention.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - // Bun does not implement `v8.setFlagsFromString`, which the scenario needs to force GC. - test.runIf(supports({ runtimes: ['node', 'deno'] }))( - 'does not keep the traces of tool calls alive for subagent conversations that never run', - async () => { - await createRunner() - .ignore('span') - .expect({ event: { message: 'pi-durable retention', extra: { toolCalls: 3, alive: 0 } } }) - .start() - .completed(); - }, - ); - }, - PI_DURABLE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-restart-abort.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('starts no run for an abort handler in a Harness that never saw the run', async () => { - await createRunner() - .ignore('span') - .expect({ - event: { - message: 'pi-durable restart abort', - extra: { status: 'unanswered', reason: 'aborted', runsStarted: 0 }, - }, - }) - .start() - .completed(); - }); - }, - PI_DURABLE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-interrupted.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('reports a provider that throws as a fault, and nothing for failed, aborted or closed runs', async () => { + createEsmTests(__dirname, 'scenario-retention.mjs', 'instrument.mjs', (createRunner, test) => { + // Bun does not implement `v8.setFlagsFromString`, which the scenario needs to force GC. + test.runIf(supports({ runtimes: ['node', 'deno'] }))( + 'does not keep the traces of tool calls alive for subagent conversations that never run', + async () => { await createRunner() .ignore('span') - .expect({ - event: event => { - const exception = event.exception?.values?.[0]; - expect(exception?.value).toBe('provider broke'); - expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); - }, - }) - .expect({ - event: event => { - expect(event.message).toBe('pi-durable interrupted done'); - }, - }) + .expect({ event: { message: 'pi-durable retention', extra: { toolCalls: 3, alive: 0 } } }) .start() .completed(); - }); + }, + ); + }); - test('ends every run that does not end with an answer', async () => { - await createRunner() - .unordered() - .expect({ - span: container => { - // 1. The model request failed: the run and the request are errors. - const chat = container.items.find(span => span.name === 'chat faux-model')!; - expect(chat.attributes[GEN_AI_INPUT_MESSAGES]?.value).toContain('Fail please.'); - expect(chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).toBe('["error"]'); - expect(chat.status).toBe('error'); - expect(chat.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('internal_error'); - const agent = container.items.find(span => span.is_segment)!; - expect(agent.name).toBe('invoke_agent'); - expect(agent.status).toBe('error'); - expect(agent.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('model_error'); - }, - }) - .expect({ - span: container => { - // 2. The provider threw: the generation faulted. - const chat = container.items.find(span => span.name === 'chat faux-model')!; - expect(chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]).toBeUndefined(); - expect(chat.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('internal_error'); - const agent = container.items.find(span => span.is_segment)!; - expect(agent.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('internal_error'); - }, - }) - .expect({ - span: container => { - // 3. Aborted while the tool ran: cancelled, which streamed spans report as ok. - const slow = container.items.find(span => span.name === 'execute_tool slow')!; - expect(slow.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"job":"abort"}'); - expect(slow.status).toBe('ok'); - expect(container.items.find(span => span.is_segment)!.status).toBe('ok'); - }, - }) - .expect({ - span: container => { - // 4. Aborted while the model request ran. - const chat = container.items.find(span => span.name === 'chat faux-model')!; - expect(chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).toBe('["aborted"]'); - expect(container.items).toHaveLength(2); - }, - }) - .expect({ - span: container => { - // 5. The Harness closed while the tool ran: the run still ends, so the trace has a segment. - const slow = container.items.find(span => span.name === 'execute_tool slow')!; - expect(slow.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"job":"close"}'); - const agent = container.items.find(span => span.is_segment)!; - expect(agent.name).toBe('invoke_agent'); - expect(slow.parent_span_id).toBe(agent.span_id); - }, - }) - .expect({ - span: container => { - // 6. The run failed, and the Harness closed right after `wait()`, before the commit that - // ended the run resolved: the run keeps its own status. - const chat = container.items.find(span => span.name === 'chat faux-model')!; - expect(chat.attributes[GEN_AI_INPUT_MESSAGES]?.value).toContain('Fail, then close.'); - const agent = container.items.find(span => span.is_segment)!; - expect(agent.status).toBe('error'); - expect(agent.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('model_error'); - }, - }) - .start() - .completed(); - }); - }, - PI_DURABLE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-concurrent.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('keeps concurrent runs apart and out of the scopes of the requests that submitted them', async () => { - const runTraceIds = new Set(); - const errorTraceIds = new Set(); + createEsmTests(__dirname, 'scenario-restart-abort.mjs', 'instrument.mjs', (createRunner, test) => { + test('starts no run for an abort handler in a Harness that never saw the run', async () => { + await createRunner() + .ignore('span') + .expect({ + event: { + message: 'pi-durable restart abort', + extra: { status: 'unanswered', reason: 'aborted', runsStarted: 0 }, + }, + }) + .start() + .completed(); + }); + }); - await createRunner() - .unordered() - .expect({ - span: container => { - const spans = container.items; - const agent = spans.find(span => span.is_segment)!; - expect(agent.name).toBe('invoke_agent'); - const chats = spans.filter(span => span.name === 'chat faux-model'); - expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))[0].parts[0].content).toBe( - 'request A', - ); - // The run's own conversation id, not the one the request set on its scope. - const conversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; - expect(conversationId).toMatch(UUID); - for (const span of spans) { - expect(span.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); - expect(span.attributes['request']).toBeUndefined(); - } - expect( - spans.find(span => span.name === 'execute_tool work')!.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value, - ).toBe('{"tag":"request A"}'); - runTraceIds.add(agent.trace_id); - }, - }) - .expect({ - span: container => { - const spans = container.items; - const agent = spans.find(span => span.is_segment)!; - expect(agent.name).toBe('invoke_agent'); - const chats = spans.filter(span => span.name === 'chat faux-model'); - expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))[0].parts[0].content).toBe( - 'request B', - ); - const conversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; - expect(conversationId).toMatch(UUID); - for (const span of spans) { - expect(span.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); - expect(span.attributes['request']).toBeUndefined(); - } - expect( - spans.find(span => span.name === 'execute_tool work')!.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value, - ).toBe('{"tag":"request B"}'); - runTraceIds.add(agent.trace_id); - }, - }) - .expect({ - event: event => { - expect(event.exception?.values?.[0]?.value).toBe('work failed for request A'); - expect(event.tags?.request).toBeUndefined(); - errorTraceIds.add(String(event.contexts?.trace?.trace_id)); - }, - }) - .expect({ - event: event => { - expect(event.exception?.values?.[0]?.value).toBe('work failed for request B'); - expect(event.tags?.request).toBeUndefined(); - errorTraceIds.add(String(event.contexts?.trace?.trace_id)); - }, - }) - .start() - .completed(); + createEsmTests(__dirname, 'scenario-interrupted.mjs', 'instrument.mjs', (createRunner, test) => { + test('reports a provider that throws as a fault, and nothing for failed, aborted or closed runs', async () => { + await createRunner() + .ignore('span') + .expect({ + event: event => { + const exception = event.exception?.values?.[0]; + expect(exception?.value).toBe('provider broke'); + expect(exception?.mechanism).toEqual({ type: 'auto.ai.pi_durable', handled: false }); + }, + }) + .expect({ + event: event => { + expect(event.message).toBe('pi-durable interrupted done'); + }, + }) + .start() + .completed(); + }); + + test('ends every run that does not end with an answer', async () => { + await createRunner() + .unordered() + .expect({ + span: container => { + // 1. The model request failed: the run and the request are errors. + const chat = container.items.find(span => span.name === 'chat faux-model')!; + expect(chat.attributes[GEN_AI_INPUT_MESSAGES]?.value).toContain('Fail please.'); + expect(chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).toBe('["error"]'); + expect(chat.status).toBe('error'); + expect(chat.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('internal_error'); + const agent = container.items.find(span => span.is_segment)!; + expect(agent.name).toBe('invoke_agent'); + expect(agent.status).toBe('error'); + expect(agent.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('model_error'); + }, + }) + .expect({ + span: container => { + // 2. The provider threw: the generation faulted. + const chat = container.items.find(span => span.name === 'chat faux-model')!; + expect(chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]).toBeUndefined(); + expect(chat.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('internal_error'); + const agent = container.items.find(span => span.is_segment)!; + expect(agent.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('internal_error'); + }, + }) + .expect({ + span: container => { + // 3. Aborted while the tool ran: cancelled, which streamed spans report as ok. + const slow = container.items.find(span => span.name === 'execute_tool slow')!; + expect(slow.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"job":"abort"}'); + expect(slow.status).toBe('ok'); + expect(container.items.find(span => span.is_segment)!.status).toBe('ok'); + }, + }) + .expect({ + span: container => { + // 4. Aborted while the model request ran. + const chat = container.items.find(span => span.name === 'chat faux-model')!; + expect(chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).toBe('["aborted"]'); + expect(container.items).toHaveLength(2); + }, + }) + .expect({ + span: container => { + // 5. The Harness closed while the tool ran: the run still ends, so the trace has a segment. + const slow = container.items.find(span => span.name === 'execute_tool slow')!; + expect(slow.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"job":"close"}'); + const agent = container.items.find(span => span.is_segment)!; + expect(agent.name).toBe('invoke_agent'); + expect(slow.parent_span_id).toBe(agent.span_id); + }, + }) + .expect({ + span: container => { + // 6. The run failed, and the Harness closed right after `wait()`, before the commit that + // ended the run resolved: the run keeps its own status. + const chat = container.items.find(span => span.name === 'chat faux-model')!; + expect(chat.attributes[GEN_AI_INPUT_MESSAGES]?.value).toContain('Fail, then close.'); + const agent = container.items.find(span => span.is_segment)!; + expect(agent.status).toBe('error'); + expect(agent.attributes[SENTRY_STATUS_MESSAGE]?.value).toBe('model_error'); + }, + }) + .start() + .completed(); + }); + }); - // Each tool error belongs to its run's trace. - expect(errorTraceIds).toEqual(runTraceIds); - }); - }, - PI_DURABLE_DEPENDENCIES, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-compaction.mjs', - 'instrument.mjs', - (createRunner, test, mode) => { - if (mode === 'cjs') { - return; - } - - test('keeps a compaction that pi-durable starts for a run inside that run', async () => { - await createRunner() - .unordered() - .expect({ - span: container => { - const spans = container.items; - const summary = spans.find(span => - String(span.attributes[GEN_AI_OUTPUT_MESSAGES]?.value).includes('Summary'), - )!; - const agent = spans.find(span => span.is_segment)!; - expect(agent.name).toBe('invoke_agent'); - expect(summary.parent_span_id).toBe(agent.span_id); - // pi-durable completes the summary without streaming; the run's own request streams. - expect(summary.attributes[GEN_AI_RESPONSE_STREAMING]).toBeUndefined(); - const answer = spans.find(span => - String(span.attributes[GEN_AI_OUTPUT_MESSAGES]?.value).includes('Third answer'), - )!; - expect(answer.parent_span_id).toBe(agent.span_id); - expect(answer.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); - }, - }) - .expect({ - span: container => expect(container.items.find(span => span.is_segment)!.name).toBe('invoke_agent'), - }) - .expect({ - span: container => expect(container.items.find(span => span.is_segment)!.name).toBe('invoke_agent'), - }) - .start() - .completed(); - }); - }, - PI_DURABLE_DEPENDENCIES, - ); + createEsmTests(__dirname, 'scenario-concurrent.mjs', 'instrument.mjs', (createRunner, test) => { + test('keeps concurrent runs apart and out of the scopes of the requests that submitted them', async () => { + const runTraceIds = new Set(); + const errorTraceIds = new Set(); + + await createRunner() + .unordered() + .expect({ + span: container => { + const spans = container.items; + const agent = spans.find(span => span.is_segment)!; + expect(agent.name).toBe('invoke_agent'); + const chats = spans.filter(span => span.name === 'chat faux-model'); + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))[0].parts[0].content).toBe( + 'request A', + ); + // The run's own conversation id, not the one the request set on its scope. + const conversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; + expect(conversationId).toMatch(UUID); + for (const span of spans) { + expect(span.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); + expect(span.attributes['request']).toBeUndefined(); + } + expect( + spans.find(span => span.name === 'execute_tool work')!.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value, + ).toBe('{"tag":"request A"}'); + runTraceIds.add(agent.trace_id); + }, + }) + .expect({ + span: container => { + const spans = container.items; + const agent = spans.find(span => span.is_segment)!; + expect(agent.name).toBe('invoke_agent'); + const chats = spans.filter(span => span.name === 'chat faux-model'); + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))[0].parts[0].content).toBe( + 'request B', + ); + const conversationId = agent.attributes[GEN_AI_CONVERSATION_ID]?.value; + expect(conversationId).toMatch(UUID); + for (const span of spans) { + expect(span.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); + expect(span.attributes['request']).toBeUndefined(); + } + expect( + spans.find(span => span.name === 'execute_tool work')!.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value, + ).toBe('{"tag":"request B"}'); + runTraceIds.add(agent.trace_id); + }, + }) + .expect({ + event: event => { + expect(event.exception?.values?.[0]?.value).toBe('work failed for request A'); + expect(event.tags?.request).toBeUndefined(); + errorTraceIds.add(String(event.contexts?.trace?.trace_id)); + }, + }) + .expect({ + event: event => { + expect(event.exception?.values?.[0]?.value).toBe('work failed for request B'); + expect(event.tags?.request).toBeUndefined(); + errorTraceIds.add(String(event.contexts?.trace?.trace_id)); + }, + }) + .start() + .completed(); + + // Each tool error belongs to its run's trace. + expect(errorTraceIds).toEqual(runTraceIds); + }); + }); + + createEsmTests(__dirname, 'scenario-compaction.mjs', 'instrument.mjs', (createRunner, test) => { + test('keeps a compaction that pi-durable starts for a run inside that run', async () => { + await createRunner() + .unordered() + .expect({ + span: container => { + const spans = container.items; + const summary = spans.find(span => + String(span.attributes[GEN_AI_OUTPUT_MESSAGES]?.value).includes('Summary'), + )!; + const agent = spans.find(span => span.is_segment)!; + expect(agent.name).toBe('invoke_agent'); + expect(summary.parent_span_id).toBe(agent.span_id); + // pi-durable completes the summary without streaming; the run's own request streams. + expect(summary.attributes[GEN_AI_RESPONSE_STREAMING]).toBeUndefined(); + const answer = spans.find(span => + String(span.attributes[GEN_AI_OUTPUT_MESSAGES]?.value).includes('Third answer'), + )!; + expect(answer.parent_span_id).toBe(agent.span_id); + expect(answer.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); + }, + }) + .expect({ + span: container => expect(container.items.find(span => span.is_segment)!.name).toBe('invoke_agent'), + }) + .expect({ + span: container => expect(container.items.find(span => span.is_segment)!.name).toBe('invoke_agent'), + }) + .start() + .completed(); + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0fecf4b4ca2..ca8b7afe45e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -453,7 +453,7 @@ importers: version: 0.133.0(zod@4.6.7) '@google/genai': specifier: ^1.20.0 - version: 1.20.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(encoding@0.1.13)(supports-color@10.0.0) + version: 1.52.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0) '@langchain/core': specifier: ^1.2.17 version: 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.21.3)(zod@4.6.7))(ws@8.21.3) @@ -607,9 +607,21 @@ importers: '@aws-sdk/client-sqs': specifier: ^3.1046.0 version: 3.1068.0 + '@earendil-works/chord': + specifier: ^1.4.0 + version: 1.4.0 + '@earendil-works/pi-ai': + specifier: ^1.4.0 + version: 1.4.0(@aws-sdk/credential-provider-node@3.972.90)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.4)(supports-color@10.0.0)(undici@7.24.8)(ws@8.21.3)(zod@4.6.7) + '@earendil-works/pi-durable': + specifier: ^1.4.0 + version: 1.4.0(@aws-sdk/credential-provider-node@3.972.90)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.4)(supports-color@10.0.0)(undici@7.24.8)(ws@8.21.3)(zod@4.6.7) + '@flue/runtime': + specifier: 2.0.3 + version: 2.0.3(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)(typescript@7.0.2)(ws@8.21.3)(zod@4.6.7) '@google/genai': specifier: ^1.20.0 - version: 1.20.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(encoding@0.1.13)(supports-color@10.0.0) + version: 1.52.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0) '@growthbook/growthbook': specifier: ^1.6.5 version: 1.6.5 @@ -838,6 +850,9 @@ importers: unstorage: specifier: 2.0.0-alpha.7 version: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.10.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@3.7.4)(ofetch@1.5.1) + valibot: + specifier: 1.4.2 + version: 1.4.2(typescript@7.0.2) winston: specifier: ^3.17.0 version: 3.19.0 @@ -2157,7 +2172,7 @@ importers: version: 29.7.0 jsdom-worker: specifier: ^0.3.0 - version: 0.3.0(node-fetch@2.7.0(encoding@0.1.13)) + version: 0.3.0(node-fetch@3.3.2) packages/replay-worker: dependencies: @@ -2779,6 +2794,15 @@ packages: '@angular/platform-browser': 14.3.0 rxjs: ^6.5.3 || ^7.4.0 + '@anthropic-ai/sdk@0.129.0': + resolution: {integrity: sha512-MH7LB20kNpGLUpPTe2OG5XvL/KhX8HUO9XyBaFjgFk6TLS4Xjt0VPIuMKywe7POKKPslX+QxSlVok9e+8kG5Nw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@anthropic-ai/sdk@0.133.0': resolution: {integrity: sha512-jlqUN8p8ESTJj+GJnA4KjzbEfF1waEoGzRT8jwytlYGFFgv+/DUQOxlYGvYw10UzhD1kwLqYzxvA3FrTB2YWuQ==} hasBin: true @@ -2797,6 +2821,15 @@ packages: zod: optional: true + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@apm-js-collab/code-transformer-bundler-plugins@0.7.5': resolution: {integrity: sha512-/nmTAee5mBly9QiIQSGIKITv9TcEyLTQJFbYjfyWCEfYofxQgurMrT5fLDIRneSPqICucRTxgPMHmc6/q7cgfw==} engines: {node: '>=18.0.0'} @@ -2955,6 +2988,14 @@ packages: resolution: {integrity: sha512-zOXUUnilC6lgCsQtp77p/QNPmRlTES9Xi6tlDwbR6kfC/kz5PCzZckgHWm5z+8DskdwuMAbFDq61x3zr10GEEQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-bedrock-runtime@3.1127.0': + resolution: {integrity: sha512-IDl/lrPb90aH+pZFHGNDmgH9nAUQj5PlZH1sJ3w7RikctyjHSnY3oNjZhrLoaBoQn/rNK0zsP6OHEqEhj2tdLA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-bedrock-runtime@3.1159.0': resolution: {integrity: sha512-Zj744tR9wNs+jjvb7C8zDBDxjJhGJSMwIHnOc9zdsDdrRirai4CtR5iQ5BMP1qfazjH6jYKCxGpZC1BOUnslKg==} engines: {node: '>=20.0.0'} @@ -3111,6 +3152,14 @@ packages: resolution: {integrity: sha512-Ob9GCZ3YnGPws696zTYW/rLFYHa9QLh6DgTCnkHb+0jhHNBgHWM8Ui2b+yrIjkfDsRVsglyrOey6tOOG/VWxMA==} engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1127.0': + resolution: {integrity: sha512-Dv2TMWBshJ+tF6ahs2Sy5bh4Iabsd4GAQqVvE9XZmYmnoaVbpS2QKIKE/HRacc7bTtbjEEvP+laGzHvHlf1CiQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1159.0': resolution: {integrity: sha512-IJvghXXcEpqTWGjrjJwxBdEecHeXp+t9FaybRf8Zf0wYW84KN1snXBsYOdfDBBx78ChwfJxH8KIHqmhNZuDLcw==} engines: {node: '>=20.0.0'} @@ -4389,6 +4438,36 @@ packages: '@dxup/unimport@0.1.2': resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==} + '@earendil-works/chord@1.4.0': + resolution: {integrity: sha512-dTbgyz4xgDS5KAUFx1bFENqj0Cr1N3tYxJJOYStc6g6HS78FSBZydyoCwLoddiQRF+VgRS0fpgAi+iKxjZ3BQA==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-agent-core@0.83.0': + resolution: {integrity: sha512-RorGp9OH5l3ElpuC5a5ZQ2eWcchZGXflXRzVGkV99y3y6tT+LLNyxoYIdVKvTKWEObwhExeQbTH0fI2tE4iX4g==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.83.0': + resolution: {integrity: sha512-m3IZD4g3er0V8TC9+Vpgw/sjTKqcJlkcIBy/JvsgRubuuik3tAVzyugUg4rVrShIkkOT69mEd34NEqKUIsl6JQ==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-ai@1.4.0': + resolution: {integrity: sha512-hQM/C4+1w8rHYACzDDf+0vQMnEnpzIop1tptIERVgR3ZeT/hHSzBNG8PaWPgxWQjqSeKCyBZSyp8uTT6eKK1pQ==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-codemode@1.4.0': + resolution: {integrity: sha512-23CNAXpS9DuLzPMyYuvrXVVio46x4YhffCgCqaWmF7F5H8PooqjrQvX4ucF6M0BhUd+x2tV6r1J0dv8cCME4Cg==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-durable@1.4.0': + resolution: {integrity: sha512-EtIyXH1MBSXdJSkodmy+CYXuwfve3gPrD4GNCZ0XmuRWLPupwRlNQ7fdaXjRcLoKb6SWkT/yEzK+sMmrqvYK+Q==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-telemetry@1.4.0': + resolution: {integrity: sha512-MLIfowwUYIY65jSjMX82paZOrX/NTvLk6D08h+3Jq42a99RzUOewzuCR0zOlPul3D6UBfddat76VT6LvsHEs1w==} + engines: {node: '>=22.19.0'} + '@edge-runtime/primitives@6.0.0': resolution: {integrity: sha512-FqoxaBT+prPBHBwE1WXS1ocnu/VLTQyZ6NMUBAdbP7N2hsFTTxMC/jMu2D/8GAlMQfxeuppcPuCUk/HO3fpIvA==} engines: {node: '>=18'} @@ -5298,6 +5377,10 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@flue/runtime@2.0.3': + resolution: {integrity: sha512-RfWyZG9x2hlDb1264XTESX42tzzG3AA8er3XjaTIxIMh28pTD91zKd9Jh6PFXKeTkZegLsGiJKDxmiCddlyeug==} + engines: {node: '>=22.19.0'} + '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -5349,11 +5432,20 @@ packages: resolution: {integrity: sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==} engines: {node: '>=10'} - '@google/genai@1.20.0': - resolution: {integrity: sha512-QdShxO9LX35jFogy3iKprQNqgKKveux4H2QjOnyIvyHRuGi6PHiz3fjNf8Y0VPY8o5V2fHqR2XqiSVoz7yZs0w==} + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} engines: {node: '>=20.0.0'} peerDependencies: - '@modelcontextprotocol/sdk': ^1.11.4 + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@google/genai@2.21.0': + resolution: {integrity: sha512-+PDtco2/Z0ONdzCGekCoCT+O1VJS9xJQNN4XzQpXG/t3El/SWWMkCWlFRO1KmivOHPa4Q0VjUYu1HBKCZ/v33Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 peerDependenciesMeta: '@modelcontextprotocol/sdk': optional: true @@ -6114,6 +6206,14 @@ packages: engines: {node: '>=18'} hasBin: true + '@mistralai/mistralai@2.2.6': + resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@mistralai/mistralai@3.3.0': resolution: {integrity: sha512-b5NSkQ8lFBxwZfA4Vj5oHxs7Jw7VrZRI2E9g95sigQiTi3NsQ//o6y3nDXjKUPWjksfagjH3STEFCYBg19SMvg==} peerDependencies: @@ -8309,6 +8409,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@silvia-odwyer/photon@0.3.3': + resolution: {integrity: sha512-8BhUjEch4slwRe8uXnaA4vcA5uiiOTT90UMsxulOj2gN98X1p0q9Z4Ysk4DkD05uNgbR9XoSqtZ37w+33w4QKQ==} + '@simple-dom/interface@1.4.0': resolution: {integrity: sha512-l5qumKFWU0S+4ZzMaLXFU8tQZsicHEMEyAxI5kDFGhJsRqDwe0a7/iPA/GdxlGyDKseQQAgIz5kzU7eXTrlSpA==} @@ -8411,6 +8514,10 @@ packages: resolution: {integrity: sha512-/GGcPoNJ2nzGytdAanpbx7mpp9Coa8LP+/Rpmbktr8YeLuIXs9B0SEz+X3AeonHdyzOIFJdKbN7jOZ+Tj+pMGw==} engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + '@smithy/property-provider@4.2.14': resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} engines: {node: '>=18.0.0'} @@ -9356,6 +9463,11 @@ packages: peerDependencies: vue: '>=3.5.18' + '@valibot/to-json-schema@1.8.0': + resolution: {integrity: sha512-a0M+uwCuQZEPAo65NYkFSJ14O5c213KoSZmPdoCfuFvUhfULi4T2Z6Tpjv6lTVMW9RoLm74RpRAjNEa43KcN+g==} + peerDependencies: + valibot: ^1.5.0 + '@vercel/nft@1.11.0': resolution: {integrity: sha512-m1QFg+U+3yPOnP1xSYJ73UIRxLOXdts1JOhiOiyPYqEsALgrXFFINvgUaD6R6iNvaBFAjHllBCbkfx4FuOdpaA==} engines: {node: '>=20'} @@ -9732,6 +9844,10 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} + agentkeepalive@4.2.1: resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==} engines: {node: '>= 8.0.0'} @@ -9897,6 +10013,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + argparse@3.0.4: + resolution: {integrity: sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==} + args@5.0.3: resolution: {integrity: sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA==} engines: {node: '>= 6.0.0'} @@ -11111,6 +11230,10 @@ packages: resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} engines: {node: '>= 6'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@4.0.0: resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==} engines: {node: '>=14'} @@ -12407,6 +12530,10 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.4.9: resolution: {integrity: sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==} @@ -12529,6 +12656,10 @@ packages: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -12624,17 +12755,17 @@ packages: resolution: {integrity: sha512-Ms7fNifGv0XVU+6eIyL9LB7RVESeML9+cMvkwGS70xyD6w2Z80wl6RiqiJ9k1KFlJCUTQqFFc8tXmPQfSKUe8g==} engines: {node: '>=10'} - gaxios@6.7.1: - resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} - engines: {node: '>=14'} + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} gcp-metadata@4.2.1: resolution: {integrity: sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==} engines: {node: '>=10'} - gcp-metadata@6.1.1: - resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} - engines: {node: '>=14'} + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -12803,16 +12934,16 @@ packages: engines: {node: '>=0.6.0'} hasBin: true + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + google-auth-library@7.0.3: resolution: {integrity: sha512-6wJNYqY1QUr5I2lWaUkkzOT2b9OCNhNQrdFOt/bsBbGb7T7NCdEvrBsXraUm+KTUGk2xGlQ7m9RgUd4Llcw8NQ==} engines: {node: '>=10'} - google-auth-library@9.15.1: - resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} - engines: {node: '>=14'} - - google-logging-utils@0.0.2: - resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} google-p12-pem@3.1.4: @@ -12853,10 +12984,6 @@ packages: resolution: {integrity: sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==} engines: {node: '>=10'} - gtoken@7.1.0: - resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} - engines: {node: '>=14.0.0'} - gzip-size@7.0.0: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -13090,6 +13217,10 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http-proxy-agent@9.1.0: + resolution: {integrity: sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==} + engines: {node: '>= 20'} + http-proxy-middleware@2.0.10: resolution: {integrity: sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==} engines: {node: '>=12.0.0'} @@ -13119,6 +13250,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + https-proxy-agent@9.1.0: + resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==} + engines: {node: '>= 20'} + httpxy@0.5.1: resolution: {integrity: sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A==} @@ -13683,6 +13818,10 @@ packages: resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true + js-yaml@5.5.0: + resolution: {integrity: sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==} + hasBin: true + jsdoc-type-pratt-parser@4.1.0: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} @@ -13948,6 +14087,9 @@ packages: launch-editor@2.14.1: resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} + layerr@3.0.0: + resolution: {integrity: sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -15107,6 +15249,11 @@ packages: resolution: {integrity: sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==} engines: {node: '>=20'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} @@ -15119,6 +15266,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-forge@1.4.0: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} @@ -15421,6 +15572,42 @@ packages: zod: optional: true + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + openai@7.19.0: + resolution: {integrity: sha512-MX2s3u2L5racTO0CC/SWpCOasJQBCJrqLKXK+l82cAhdeF8mPMBEe/gxMm0ZFa2xpKpOFLRjxv5afYEZbBXmbQ==} + engines: {node: '>=22.0.0'} + peerDependencies: + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + undici: '>=5 <9' + ws: ^8.21.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + undici: + optional: true + ws: + optional: true + zod: + optional: true + openai@7.31.0: resolution: {integrity: sha512-ZGAzauuYb0DdB53qVbIR+X8rdmNtWgiElrrMthbZCVreLcIsQJBwzAIWUbpB2FkQu2Cj6OJ9zJR1EKMUYn4/SQ==} engines: {node: '>=22.0.0'} @@ -15700,6 +15887,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -16496,10 +16686,23 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + protobufjs@7.6.6: + resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.8: resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} + proxy-agent-negotiate@1.1.0: + resolution: {integrity: sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==} + engines: {node: '>= 20'} + peerDependencies: + kerberos: ^2.0.0 + peerDependenciesMeta: + kerberos: + optional: true + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -16554,6 +16757,9 @@ packages: quick-temp@0.1.8: resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==} + quickjs-wasi@3.6.2: + resolution: {integrity: sha512-FCqGtGOrMgzUiIrMNMA2YnsOxCNwo31dzqXvclXUC6xeT35NJLKXQJsvbeCTjvoFAwZgEAPg8U6+KAPDGXn8Mg==} + quote-unquote@1.0.0: resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} @@ -18304,6 +18510,12 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typebox@1.3.27: + resolution: {integrity: sha512-zu+jc1pcy4UiNThxikUr36f0Rybk9PEeCg/NE6adeWr/SKsdNO4EzZHYRDlv2YCVAfj3Odq3dESSo/jNyoBXzA==} + + typebox@1.3.7: + resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -18376,6 +18588,10 @@ packages: resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} engines: {node: '>=18'} + ulidx@2.4.1: + resolution: {integrity: sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==} + engines: {node: '>=16'} + ultrahtml@1.7.0: resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} @@ -20038,6 +20254,13 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 + '@anthropic-ai/sdk@0.129.0(zod@4.6.7)': + dependencies: + json-schema-to-ts: 3.1.1 + standardwebhooks: 1.1.1 + optionalDependencies: + zod: 4.6.7 + '@anthropic-ai/sdk@0.133.0(zod@4.6.7)': dependencies: json-schema-to-ts: 3.1.1 @@ -20051,6 +20274,12 @@ snapshots: optionalDependencies: zod: 4.6.7 + '@anthropic-ai/sdk@0.91.1(zod@4.6.7)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.6.7 + '@apm-js-collab/code-transformer-bundler-plugins@0.7.5(esbuild@0.28.1)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': dependencies: '@apm-js-collab/code-transformer': 0.19.0 @@ -20298,6 +20527,38 @@ snapshots: '@smithy/types': 4.19.0 tslib: 2.8.1 + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.978.7 + '@aws-sdk/credential-provider-node': 3.972.90 + '@aws-sdk/eventstream-handler-node': 3.972.39 + '@aws-sdk/middleware-eventstream': 3.972.35 + '@aws-sdk/middleware-websocket': 3.972.60 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.37.0 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1127.0': + dependencies: + '@aws-sdk/core': 3.978.7 + '@aws-sdk/credential-provider-node': 3.972.90 + '@aws-sdk/eventstream-handler-node': 3.972.39 + '@aws-sdk/middleware-eventstream': 3.972.35 + '@aws-sdk/middleware-websocket': 3.972.60 + '@aws-sdk/token-providers': 3.1127.0 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.37.0 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + '@aws-sdk/client-bedrock-runtime@3.1159.0': dependencies: '@aws-sdk/core': 3.978.7 @@ -20816,6 +21077,24 @@ snapshots: '@smithy/types': 4.19.0 tslib: 2.8.1 + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.978.7 + '@aws-sdk/nested-clients': 3.997.52 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.37.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1127.0': + dependencies: + '@aws-sdk/core': 3.978.7 + '@aws-sdk/nested-clients': 3.997.52 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.37.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.1159.0': dependencies: '@aws-sdk/core': 3.978.7 @@ -22875,6 +23154,100 @@ snapshots: '@dxup/unimport@0.1.2': {} + '@earendil-works/chord@1.4.0': + dependencies: + '@earendil-works/pi-telemetry': 1.4.0 + typebox: 1.3.27 + + '@earendil-works/pi-agent-core@0.83.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)(ws@8.21.3)(zod@4.6.7)': + dependencies: + '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)(ws@8.21.3)(zod@4.6.7) + diff: 8.0.4 + ignore: 7.0.5 + typebox: 1.3.7 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.83.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)(ws@8.21.3)(zod@4.6.7)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@4.6.7) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0) + '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2(supports-color@10.0.0) + https-proxy-agent: 7.0.6(supports-color@10.0.0) + openai: 6.26.0(ws@8.21.3)(zod@4.6.7) + partial-json: 0.1.7 + typebox: 1.3.7 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@1.4.0(@aws-sdk/credential-provider-node@3.972.90)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.4)(supports-color@10.0.0)(undici@7.24.8)(ws@8.21.3)(zod@4.6.7)': + dependencies: + '@anthropic-ai/sdk': 0.129.0(zod@4.6.7) + '@aws-sdk/client-bedrock-runtime': 3.1127.0 + '@earendil-works/pi-telemetry': 1.4.0 + '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0) + '@smithy/node-http-handler': 4.12.1 + http-proxy-agent: 9.1.0(supports-color@10.0.0) + https-proxy-agent: 9.1.0(supports-color@10.0.0) + openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.90)(@smithy/signature-v4@5.7.4)(undici@7.24.8)(ws@8.21.3)(zod@4.6.7) + partial-json: 0.1.7 + typebox: 1.3.27 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - '@modelcontextprotocol/sdk' + - '@smithy/hash-node' + - '@smithy/signature-v4' + - bufferutil + - kerberos + - supports-color + - undici + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-codemode@1.4.0': + dependencies: + quickjs-wasi: 3.6.2 + + '@earendil-works/pi-durable@1.4.0(@aws-sdk/credential-provider-node@3.972.90)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.4)(supports-color@10.0.0)(undici@7.24.8)(ws@8.21.3)(zod@4.6.7)': + dependencies: + '@earendil-works/chord': 1.4.0 + '@earendil-works/pi-ai': 1.4.0(@aws-sdk/credential-provider-node@3.972.90)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.4)(supports-color@10.0.0)(undici@7.24.8)(ws@8.21.3)(zod@4.6.7) + '@earendil-works/pi-codemode': 1.4.0 + '@earendil-works/pi-telemetry': 1.4.0 + '@silvia-odwyer/photon': 0.3.3 + diff: 8.0.4 + typebox: 1.3.27 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - '@modelcontextprotocol/sdk' + - '@smithy/hash-node' + - '@smithy/signature-v4' + - bufferutil + - kerberos + - supports-color + - undici + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-telemetry@1.4.0': {} + '@edge-runtime/primitives@6.0.0': {} '@edge-runtime/types@4.0.0': @@ -23542,6 +23915,26 @@ snapshots: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.4.0 + '@flue/runtime@2.0.3(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)(typescript@7.0.2)(ws@8.21.3)(zod@4.6.7)': + dependencies: + '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)(ws@8.21.3)(zod@4.6.7) + '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)(ws@8.21.3)(zod@4.6.7) + '@hono/node-server': 2.0.10(hono@4.13.11) + '@modelcontextprotocol/client': 2.0.0 + '@valibot/to-json-schema': 1.8.0(valibot@1.4.2(typescript@7.0.2)) + hono: 4.13.11 + js-yaml: 5.5.0 + ulidx: 2.4.1 + valibot: 1.4.2(typescript@7.0.2) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - typescript + - utf-8-validate + - ws + - zod + '@gar/promisify@1.1.3': {} '@glimmer/env@0.1.7': {} @@ -23634,15 +24027,29 @@ snapshots: '@google-cloud/promisify@2.0.3': {} - '@google/genai@1.20.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(encoding@0.1.13)(supports-color@10.0.0)': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)': dependencies: - google-auth-library: 9.15.1(encoding@0.1.13)(supports-color@10.0.0) + google-auth-library: 10.9.1(supports-color@10.0.0) + p-retry: 4.6.2 + protobufjs: 7.6.6 + ws: 8.21.3 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@google/genai@2.21.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0)': + dependencies: + google-auth-library: 10.9.1(supports-color@10.0.0) + p-retry: 4.6.2 + protobufjs: 7.6.6 ws: 8.21.3 optionalDependencies: '@modelcontextprotocol/sdk': 1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7) transitivePeerDependencies: - bufferutil - - encoding - supports-color - utf-8-validate @@ -24391,6 +24798,18 @@ snapshots: - encoding - supports-color + '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/semantic-conventions': 1.43.0 + ws: 8.21.3 + zod: 4.6.7 + zod-to-json-schema: 3.25.2(zod@4.6.7) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@mistralai/mistralai@3.3.0(@aws-sdk/client-s3@3.1068.0)(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@cfworker/json-schema@4.1.1)(@opentelemetry/api@1.9.0)(supports-color@10.0.0)': dependencies: '@modelcontextprotocol/sdk': 1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7) @@ -26455,6 +26874,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@silvia-odwyer/photon@0.3.3': {} + '@simple-dom/interface@1.4.0': {} '@simple-git/args-pathspec@1.0.3': {} @@ -26620,6 +27041,12 @@ snapshots: '@smithy/types': 4.19.0 tslib: 2.8.1 + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.37.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + '@smithy/property-provider@4.2.14': dependencies: '@smithy/types': 4.19.0 @@ -27793,6 +28220,10 @@ snapshots: unhead: 2.1.17 vue: 3.5.44(typescript@7.0.2) + '@valibot/to-json-schema@1.8.0(valibot@1.4.2(typescript@7.0.2))': + dependencies: + valibot: 1.4.2(typescript@7.0.2) + '@vercel/nft@1.11.0(encoding@0.1.13)(rollup@4.62.3)(supports-color@10.0.0)': dependencies: '@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)(supports-color@10.0.0) @@ -28379,6 +28810,8 @@ snapshots: agent-base@7.1.3: {} + agent-base@9.0.0: {} + agentkeepalive@4.2.1(supports-color@10.0.0): dependencies: debug: 4.3.4(supports-color@10.0.0) @@ -28550,6 +28983,8 @@ snapshots: argparse@2.0.1: {} + argparse@3.0.4: {} + args@5.0.3: dependencies: camelcase: 5.0.0 @@ -30074,6 +30509,8 @@ snapshots: data-uri-to-buffer@3.0.1: {} + data-uri-to-buffer@4.0.1: {} + data-urls@4.0.0: dependencies: abab: 2.0.6 @@ -31625,6 +32062,11 @@ snapshots: fecha@4.2.3: {} + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.4.9: {} fflate@0.8.3: {} @@ -31790,6 +32232,10 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -31922,15 +32368,12 @@ snapshots: - encoding - supports-color - gaxios@6.7.1(encoding@0.1.13)(supports-color@10.0.0): + gaxios@7.3.1(supports-color@10.0.0): dependencies: extend: 3.0.2 https-proxy-agent: 7.0.6(supports-color@10.0.0) - is-stream: 2.0.1 - node-fetch: 2.7.0(encoding@0.1.13) - uuid: 9.0.1 + node-fetch: 3.3.2 transitivePeerDependencies: - - encoding - supports-color gcp-metadata@4.2.1(encoding@0.1.13)(supports-color@10.0.0): @@ -31941,13 +32384,12 @@ snapshots: - encoding - supports-color - gcp-metadata@6.1.1(encoding@0.1.13)(supports-color@10.0.0): + gcp-metadata@8.1.2(supports-color@10.0.0): dependencies: - gaxios: 6.7.1(encoding@0.1.13)(supports-color@10.0.0) - google-logging-utils: 0.0.2 + gaxios: 7.3.1(supports-color@10.0.0) + google-logging-utils: 1.1.3 json-bigint: 1.0.0 transitivePeerDependencies: - - encoding - supports-color generate-function@2.3.1: @@ -32156,34 +32598,33 @@ snapshots: dependencies: minimist: 1.2.8 - google-auth-library@7.0.3(encoding@0.1.13)(supports-color@10.0.0): + google-auth-library@10.9.1(supports-color@10.0.0): dependencies: - arrify: 2.0.1 base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - fast-text-encoding: 1.0.3 - gaxios: 4.2.0(encoding@0.1.13)(supports-color@10.0.0) - gcp-metadata: 4.2.1(encoding@0.1.13)(supports-color@10.0.0) - gtoken: 5.2.1(encoding@0.1.13)(supports-color@10.0.0) + gaxios: 7.3.1(supports-color@10.0.0) + gcp-metadata: 8.1.2(supports-color@10.0.0) + google-logging-utils: 1.1.3 jws: 4.0.1 - lru-cache: 6.0.0 transitivePeerDependencies: - - encoding - supports-color - google-auth-library@9.15.1(encoding@0.1.13)(supports-color@10.0.0): + google-auth-library@7.0.3(encoding@0.1.13)(supports-color@10.0.0): dependencies: + arrify: 2.0.1 base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 6.7.1(encoding@0.1.13)(supports-color@10.0.0) - gcp-metadata: 6.1.1(encoding@0.1.13)(supports-color@10.0.0) - gtoken: 7.1.0(encoding@0.1.13)(supports-color@10.0.0) + fast-text-encoding: 1.0.3 + gaxios: 4.2.0(encoding@0.1.13)(supports-color@10.0.0) + gcp-metadata: 4.2.1(encoding@0.1.13)(supports-color@10.0.0) + gtoken: 5.2.1(encoding@0.1.13)(supports-color@10.0.0) jws: 4.0.1 + lru-cache: 6.0.0 transitivePeerDependencies: - encoding - supports-color - google-logging-utils@0.0.2: {} + google-logging-utils@1.1.3: {} google-p12-pem@3.1.4: dependencies: @@ -32220,14 +32661,6 @@ snapshots: - encoding - supports-color - gtoken@7.1.0(encoding@0.1.13)(supports-color@10.0.0): - dependencies: - gaxios: 6.7.1(encoding@0.1.13)(supports-color@10.0.0) - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - gzip-size@7.0.0: dependencies: duplexer: 0.1.2 @@ -32596,6 +33029,15 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-agent@9.1.0(supports-color@10.0.0): + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@10.0.0) + proxy-agent-negotiate: 1.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + http-proxy-middleware@2.0.10(@types/express@4.17.21)(debug@4.4.3(supports-color@10.0.0)): dependencies: '@types/http-proxy': 1.17.14 @@ -32661,6 +33103,15 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@9.1.0(supports-color@10.0.0): + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@10.0.0) + proxy-agent-negotiate: 1.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + httpxy@0.5.1: {} human-signals@1.1.1: {} @@ -33199,14 +33650,18 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@5.5.0: + dependencies: + argparse: 3.0.4 + jsdoc-type-pratt-parser@4.1.0: {} jsdoc-type-pratt-parser@7.2.0: {} - jsdom-worker@0.3.0(node-fetch@2.7.0(encoding@0.1.13)): + jsdom-worker@0.3.0(node-fetch@3.3.2): dependencies: mitt: 3.0.1 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 3.3.2 uuid-v4: 0.1.0 jsdom@21.1.2(supports-color@10.0.0): @@ -33553,6 +34008,8 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.12.0 + layerr@3.0.0: {} + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -35311,6 +35768,8 @@ snapshots: node-cron@4.6.0: {} + node-domexception@1.0.0: {} + node-fetch-native@1.6.7: {} node-fetch@2.7.0(encoding@0.1.13): @@ -35319,6 +35778,12 @@ snapshots: optionalDependencies: encoding: 0.1.13 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-forge@1.4.0: {} node-gyp-build-optional-packages@5.2.2: @@ -35897,6 +36362,19 @@ snapshots: ws: 8.21.3 zod: 4.6.7 + openai@6.26.0(ws@8.21.3)(zod@4.6.7): + optionalDependencies: + ws: 8.21.3 + zod: 4.6.7 + + openai@7.19.0(@aws-sdk/credential-provider-node@3.972.90)(@smithy/signature-v4@5.7.4)(undici@7.24.8)(ws@8.21.3)(zod@4.6.7): + optionalDependencies: + '@aws-sdk/credential-provider-node': 3.972.90 + '@smithy/signature-v4': 5.7.4 + undici: 7.24.8 + ws: 8.21.3 + zod: 4.6.7 + openai@7.31.0(@aws-sdk/credential-provider-node@3.972.90)(@smithy/signature-v4@5.7.4)(undici@7.24.8)(ws@8.21.3)(zod@4.6.7): optionalDependencies: '@aws-sdk/credential-provider-node': 3.972.90 @@ -36299,6 +36777,8 @@ snapshots: parseurl@1.3.3: {} + partial-json@0.1.7: {} + pascal-case@3.1.2: dependencies: no-case: 3.0.4 @@ -37268,11 +37748,27 @@ snapshots: property-information@7.1.0: {} + protobufjs@7.6.6: + dependencies: + '@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 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 20.19.43 + long: 5.3.2 + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-agent-negotiate@1.1.0: {} + proxy-from-env@2.1.0: {} proxy@2.1.1(supports-color@10.0.0): @@ -37327,6 +37823,8 @@ snapshots: rimraf: 2.7.1 underscore.string: 3.3.6 + quickjs-wasi@3.6.2: {} + quote-unquote@1.0.0: {} radix3@1.1.2: {} @@ -39425,6 +39923,10 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typebox@1.3.27: {} + + typebox@1.3.7: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -39512,6 +40014,10 @@ snapshots: uint8array-extras@1.4.0: {} + ulidx@2.4.1: + dependencies: + layerr: 3.0.0 + ultrahtml@1.7.0: {} unbox-primitive@1.1.0: @@ -39978,6 +40484,10 @@ snapshots: optionalDependencies: typescript: 6.0.3 + valibot@1.4.2(typescript@7.0.2): + optionalDependencies: + typescript: 7.0.2 + validate-html-nesting@1.2.2: {} validate-npm-package-license@3.0.4: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f0da0985135e..69f59d9ab5e3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -99,6 +99,7 @@ packageExtensions: # `false` only for scripts that just print a message. allowBuilds: '@apollo/protobufjs': true + '@google/genai': false '@nestjs/core': false '@parcel/watcher': true '@prisma/client': true @@ -112,6 +113,7 @@ allowBuilds: nice-napi: true nx: true prisma: true + protobufjs: false sharp: true vue-demi: true workerd: true From 6393bbb049eccade571d0e359341cc098513469f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 30 Oct 2026 11:17:18 +0100 Subject: [PATCH 2/2] test(node-integration-tests): Use conventions constants in Flue assertions Co-Authored-By: Claude Opus 5.5 --- .../suites/tracing/flue/test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts index ed1365a7ec4a..83c73526b43e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts @@ -14,7 +14,10 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; +import { GEN_AI_CHAT, GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { afterAll, describe, expect } from 'vitest'; import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmTests } from '../../../utils/runner'; @@ -39,7 +42,7 @@ describe.runIf(supports({ min: 22 }))('Flue integration', () => { // Counted rather than looked up: the interceptor skips the submission wrapper // operation, so one dispatch opens exactly one agent span, and each turn and tool call // is spanned once. `find` passes just as happily on a duplicate. - expect(spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.ai.flue')).toHaveLength(4); + expect(spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.flue')).toHaveLength(4); const agents = spans.filter(span => span.name === 'invoke_agent Hello'); const chats = spans.filter(span => span.name === 'chat faux-model'); @@ -50,7 +53,7 @@ describe.runIf(supports({ min: 22 }))('Flue integration', () => { expect(chats).toHaveLength(2); const agent = agents[0]!; - expect(agent.attributes['sentry.op']?.value).toBe('gen_ai.invoke_agent'); + expect(agent.attributes[SENTRY_OP]?.value).toBe(GEN_AI_INVOKE_AGENT); expect(agent.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); expect(agent.attributes[GEN_AI_AGENT_NAME]?.value).toBe('Hello'); expect(agent.parent_span_id).toBe(root.span_id); @@ -61,8 +64,8 @@ describe.runIf(supports({ min: 22 }))('Flue integration', () => { // Both turns, not just the first: they leave the provider by different paths (a tool // call, then a final answer) and resolve their parent through separate tracker lookups. for (const chat of chats) { - expect(chat.attributes['sentry.op']?.value).toBe('gen_ai.chat'); - expect(chat.attributes['sentry.origin']?.value).toBe('auto.ai.flue'); + expect(chat.attributes[SENTRY_OP]?.value).toBe(GEN_AI_CHAT); + expect(chat.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.flue'); expect(chat.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); expect(chat.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe(conversationId); expect(chat.parent_span_id).toBe(agent.span_id); @@ -84,8 +87,8 @@ describe.runIf(supports({ min: 22 }))('Flue integration', () => { // The tool has to actually run: a schema mismatch still produces a correctly named and // parented span, so only the status separates a real call from a rejected one. expect(tool.status).toBe('ok'); - expect(tool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); - expect(tool.attributes['sentry.origin']?.value).toBe('auto.ai.flue'); + expect(tool.attributes[SENTRY_OP]?.value).toBe(GEN_AI_EXECUTE_TOOL); + expect(tool.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.flue'); expect(tool.attributes[GEN_AI_TOOL_NAME]?.value).toBe('get_weather'); // Tool spans are siblings of `chat` under the agent invocation, matching how Flue's