diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/package.json index 6699912023e6..80c16c600e11 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/package.json @@ -12,8 +12,8 @@ "test:assert": "playwright test" }, "dependencies": { - "@earendil-works/pi-ai": "1.0.0", - "@earendil-works/pi-durable": "1.0.0", + "@earendil-works/pi-ai": "1.2.0", + "@earendil-works/pi-durable": "1.2.0", "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "agents": "0.26.0" }, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/assistant.ts b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/assistant.ts index bd0d25a8c5b3..93e1236190cf 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/assistant.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/assistant.ts @@ -47,7 +47,7 @@ export class Assistant extends Agent { // The manual span should nest under the SDK's `execute_tool` span. execute: async args => Sentry.startSpan({ name: 'resolve-weather', attributes: { 'weather.city': args.city } }, () => ({ - content: [{ type: 'text', text: `It is 21 degrees and sunny in ${args.city}.` }], + output: [{ type: 'text', text: `It is 21 degrees and sunny in ${args.city}.` }], })), }), defineTool({ @@ -67,7 +67,7 @@ export class Assistant extends Agent { execute: async args => { const marker = `crashed:${args.job}`; if (await this.ctx.storage.get(marker)) { - return { content: [{ type: 'text', text: `Step of job ${args.job} completed.` }] }; + return { output: [{ type: 'text', text: `Step of job ${args.job} completed.` }] }; } await this.ctx.storage.put(marker, true); await this.ctx.storage.sync(); diff --git a/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json b/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json index eb9822667b3a..33bc22b4e00f 100644 --- a/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json +++ b/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json @@ -11,9 +11,9 @@ "test:assert": "OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test" }, "dependencies": { - "@earendil-works/chord": "1.0.0", - "@earendil-works/pi-ai": "1.0.0", - "@earendil-works/pi-durable": "1.0.0", + "@earendil-works/chord": "1.2.0", + "@earendil-works/pi-ai": "1.2.0", + "@earendil-works/pi-durable": "1.2.0", "@sentry/node": "file:../../packed/sentry-node-packed.tgz" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/node-pi-durable/src/server.mjs b/dev-packages/e2e-tests/test-applications/node-pi-durable/src/server.mjs index 79892991f03d..c71c9f4096bf 100644 --- a/dev-packages/e2e-tests/test-applications/node-pi-durable/src/server.mjs +++ b/dev-packages/e2e-tests/test-applications/node-pi-durable/src/server.mjs @@ -38,7 +38,7 @@ const tools = [ // The manual span should nest under the SDK's `execute_tool` span. execute: async args => Sentry.startSpan({ name: 'resolve-weather', attributes: { 'weather.city': args.city } }, () => ({ - content: [{ type: 'text', text: `It is 21 degrees and sunny in ${args.city}.` }], + output: [{ type: 'text', text: `It is 21 degrees and sunny in ${args.city}.` }], })), }), defineTool({ @@ -60,7 +60,7 @@ const tools = [ try { writeFileSync(`${dataDir}crashed-${args.job}`, 'crashed once', { flag: 'wx' }); } catch { - return { content: [{ type: 'text', text: `Step of job ${args.job} completed.` }] }; + return { output: [{ type: 'text', text: `Step of job ${args.job} completed.` }] }; } process.exit(CRASH_EXIT_CODE); }, @@ -80,14 +80,14 @@ const tools = [ const request = { type: 'input', content: args.task, requestId: `delegate:${api.taskId}` }; const settled = await (await child.submit(request, toolContext)).wait(toolContext); if (settled.status !== 'done') { - return { isError: true, content: [{ type: 'text', text: `Subagent ended: ${settled.reason}` }] }; + return { isError: true, output: [{ type: 'text', text: `Subagent ended: ${settled.reason}` }] }; } const answer = await api.commit(tx => tx.entry(AssistantEntry, settled.answer), toolContext); const text = (answer?.model?.[0]?.content ?? []) .filter(part => part.type === 'text') .map(part => part.text) .join(''); - return { content: [{ type: 'text', text }] }; + return { output: [{ type: 'text', text }] }; }, }), ]; diff --git a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-interrupted.mjs b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-interrupted.mjs index f0efbea32483..9d15ed6f3a47 100644 --- a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-interrupted.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-interrupted.mjs @@ -65,7 +65,7 @@ registry.install( await new Promise((_, reject) => toolContext.abortSignal.addEventListener('abort', () => reject(new Error('tool aborted'))), ); - return { content: [] }; + return { output: [] }; }, }), ], diff --git a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-retention.mjs b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-retention.mjs index dc84d6387ba9..8887587a07c2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-retention.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-retention.mjs @@ -46,7 +46,7 @@ registry.install( tx => tx.createConversation({ ownership: { kind: 'task', taskId: api.taskId } }), toolContext, ); - return { content: [{ type: 'text', text: 'Subagent created.' }] }; + return { output: [{ type: 'text', text: 'Subagent created.' }] }; }, }), ], diff --git a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-subagent.mjs b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-subagent.mjs index da80b508f252..7e6f3413d3dd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-subagent.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-subagent.mjs @@ -70,7 +70,7 @@ registry.install( const settled = await ( await child.submit({ type: 'input', content: args.task }, toolContext) ).wait(toolContext); - return { content: [{ type: 'text', text: await answerText(api, settled, toolContext) }] }; + return { output: [{ type: 'text', text: await answerText(api, settled, toolContext) }] }; }, }), defineTool({ @@ -89,14 +89,14 @@ registry.install( const settled = await ( await child.submit({ type: 'input', content: 'Forked task.' }, toolContext) ).wait(toolContext); - return { content: [{ type: 'text', text: await answerText(api, settled, toolContext) }] }; + return { output: [{ type: 'text', text: await answerText(api, settled, toolContext) }] }; }, }), defineTool({ name: 'inner', description: 'A plain tool.', parameters: Type.Object({}), - execute: async () => ({ content: [{ type: 'text', text: 'INNER RESULT' }] }), + execute: async () => ({ output: [{ type: 'text', text: 'INNER RESULT' }] }), }), defineTool({ name: 'batch', @@ -105,7 +105,7 @@ registry.install( execute: async (_args, api, toolContext) => { const inner = (await api.agent(toolContext)).tools.find(tool => tool.name === 'inner'); await inner.execute({}, api, toolContext); - return { content: [{ type: 'text', text: 'OUTER RESULT' }] }; + return { output: [{ type: 'text', text: 'OUTER RESULT' }] }; }, }), ], diff --git a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-tools.mjs b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-tools.mjs index fc42ebf2091f..fd8a977149fc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-tools.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario-tools.mjs @@ -73,7 +73,7 @@ registry.install( name: 'secret', description: 'Returns a secret.', parameters: Type.Object({}), - execute: async () => ({ content: [{ type: 'text', text: 'original secret value' }] }), + execute: async () => ({ output: [{ type: 'text', text: 'original secret value' }] }), }), defineTool({ name: 'fail_now', @@ -87,9 +87,7 @@ registry.install( hooks: [ hook(ToolTask, { afterTool: (call, result) => - call.name === 'secret' - ? { ...result, content: [{ type: 'text', text: 'redacted by afterTool' }] } - : undefined, + call.name === 'secret' ? { ...result, output: [{ type: 'text', text: 'redacted by afterTool' }] } : undefined, }), ], }), diff --git a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario.mjs index 42309d5ba2d4..4c2dc1c87172 100644 --- a/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/pi-durable/scenario.mjs @@ -38,7 +38,7 @@ registry.install( name: 'get_weather', description: 'Get the current weather for a city.', parameters: Type.Object({ city: Type.String() }), - execute: async args => ({ content: [{ type: 'text', text: `It is 21 degrees and sunny in ${args.city}.` }] }), + execute: async args => ({ output: [{ type: 'text', text: `It is 21 degrees and sunny in ${args.city}.` }] }), }), defineTool({ name: 'broken', 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 4333c9e0f415..068b389c60d2 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 @@ -30,9 +30,9 @@ import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runn // instead, guarded by the `min: 22` skip below. const PI_DURABLE_DEPENDENCIES = { additionalDependencies: { - '@earendil-works/pi-durable': '^1.0.0', - '@earendil-works/pi-ai': '^1.0.0', - '@earendil-works/chord': '^1.0.0', + '@earendil-works/pi-durable': '^1.2.0', + '@earendil-works/pi-ai': '^1.2.0', + '@earendil-works/chord': '^1.2.0', }, }; diff --git a/packages/server-utils/src/ai/pi-durable/tools.ts b/packages/server-utils/src/ai/pi-durable/tools.ts index 5d19f583dc14..8ccf21155549 100644 --- a/packages/server-utils/src/ai/pi-durable/tools.ts +++ b/packages/server-utils/src/ai/pi-durable/tools.ts @@ -138,7 +138,7 @@ export function instrumentTool(tool: PiTool, runs: PiRuns, options: GenAiOptions call.endTimestamp = timestampInSeconds(); if (!calls) { // No result entry follows a call outside a tool task phase, nor a nested call. - finishToolCall(call, { content: result?.content, isError: result?.isError }); + finishToolCall(call, { content: result?.output ?? result?.content, isError: result?.isError }); } return result; } catch (error) { diff --git a/packages/server-utils/src/ai/pi-durable/types.ts b/packages/server-utils/src/ai/pi-durable/types.ts index 6244b18af1e5..7c87317eb26a 100644 --- a/packages/server-utils/src/ai/pi-durable/types.ts +++ b/packages/server-utils/src/ai/pi-durable/types.ts @@ -50,6 +50,9 @@ export interface PiModels { } export interface PiToolExecutionResult { + /** The content blocks the tool returns for the model, in pi-durable 1.2.0 and later. */ + output?: unknown[]; + /** The content blocks the tool returns for the model, in pi-durable versions before 1.2.0. */ content?: unknown[]; isError?: boolean; } diff --git a/packages/server-utils/test/ai/lib/tracing/pi-durable.test.ts b/packages/server-utils/test/ai/lib/tracing/pi-durable.test.ts index c2b8f6e9b68e..d23b3310785e 100644 --- a/packages/server-utils/test/ai/lib/tracing/pi-durable.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/pi-durable.test.ts @@ -617,6 +617,17 @@ describe('instrumentPiDurableHarnessOptions', () => { }); }); + it('reads the result of a call outside a tool phase from `output`', async () => { + const tool: PiTool = { + name: 'get_weather', + execute: async () => ({ output: [{ type: 'text', text: 'sunny' }] }), + }; + + await instrumentTool(tool, createRuns(), {}).execute({}, { callId: 'call_1' }, undefined); + + expect(spanToJSON(endedSpans[0]!).attributes[GEN_AI_TOOL_CALL_RESULT]).toBe('sunny'); + }); + it.each([ ['no content blocks', []], ['an empty text block', [{ type: 'text', text: '' }]],