diff --git a/src/callLifecycle.ts b/src/callLifecycle.ts index e2e703a..5985421 100644 --- a/src/callLifecycle.ts +++ b/src/callLifecycle.ts @@ -7,8 +7,8 @@ import { ATTR, addPermissionRequestEvent, addPermissionResolvedEvent, - assistantOutputMessages, jsonStr, + recordOutput, } from './genaiSpans.js'; import type { SpanParent } from './genaiSpans.js'; import { VERSION } from './setup.js'; @@ -378,7 +378,7 @@ function finishAgentSpan( const output = outcome.ok ? outcome.output : outcome.error; if (output !== undefined && output !== null && output !== '') { const text = typeof output === 'string' ? output : jsonStr(output); - call.span.setAttributes({ [ATTR.OUTPUT_MESSAGES]: assistantOutputMessages([text]) }); + recordOutput(call.span, [text]); } if (outcome.ok) { call.span.end(endTime ? { endTime } : undefined); diff --git a/src/genaiSpans.ts b/src/genaiSpans.ts index 3c2bb52..fe130c9 100644 --- a/src/genaiSpans.ts +++ b/src/genaiSpans.ts @@ -5,7 +5,7 @@ // Attribute-key constants and formatting helpers typed against the `weave` SDK. import type { Attributes } from '@opentelemetry/api'; -import type { MessagePart, SubAgent, Tool, Turn, Usage } from 'weave'; +import type { Message, MessagePart, SubAgent, Tool, Turn, Usage } from 'weave'; import { isTextBlock, isThinkingBlock, isRedactedThinkingBlock, isToolUseBlock } from './parser.js'; import type { UsageSummary } from './parser.js'; @@ -107,10 +107,20 @@ export function jsonStr(v: unknown): string { } } -/** `gen_ai.output.messages` JSON for plain assistant text(s), the shape used on - * turn and subagent `invoke_agent` spans (chat spans carry parts instead). */ -export function assistantOutputMessages(texts: string[]): string { - return jsonStr(texts.map((content) => ({ role: 'assistant', content }))); +function assistantMessages(texts: string[]): Message[] { + return texts.map((content) => ({ role: 'assistant', content })); +} + +/** Record assistant text(s) and model onto a turn or subagent `invoke_agent` + * span; the SDK serializes them at `end()` (chat spans carry parts instead). + * Skipped when there is nothing to record, because `record()` warns once the + * span has already ended. */ +export function recordOutput(span: SpanParent, texts: string[], model?: string): void { + if (!texts.length && !model) return; + span.record({ + outputMessages: texts.length ? assistantMessages(texts) : undefined, + model, + }); } /** Parse an ISO timestamp; returns undefined for missing or unparseable input. */ diff --git a/src/hookHandler.ts b/src/hookHandler.ts index bd84113..49f3271 100644 --- a/src/hookHandler.ts +++ b/src/hookHandler.ts @@ -50,7 +50,7 @@ import type { TracedCall, } from './callLifecycle.js'; import type { CompactionAttrs } from './genaiSpans.js'; -import { ATTR, assistantOutputMessages, snippet } from './genaiSpans.js'; +import { ATTR, recordOutput, snippet } from './genaiSpans.js'; import type { SpanParent } from './genaiSpans.js'; import { parseSessionFd } from './parser.js'; import { Session } from './session.js'; @@ -852,9 +852,7 @@ export class HookHandler { match.call.span.setAttributes({ [ATTR.RESPONSE_MODEL]: transcript.model }); } if (!match.call.toolUseId && text) { - match.call.span.setAttributes({ - [ATTR.OUTPUT_MESSAGES]: assistantOutputMessages([text]), - }); + recordOutput(match.call.span, [text]); } recordAgentStop(session.calls, match); } else if (recovered) { @@ -862,9 +860,7 @@ export class HookHandler { recovered.span.setAttributes({ [ATTR.RESPONSE_MODEL]: transcript.model }); } if (text) { - recovered.span.setAttributes({ - [ATTR.OUTPUT_MESSAGES]: assistantOutputMessages([text]), - }); + recordOutput(recovered.span, [text]); } } diff --git a/src/session.ts b/src/session.ts index b500efb..71a56e0 100644 --- a/src/session.ts +++ b/src/session.ts @@ -9,9 +9,9 @@ import * as weave from 'weave'; import { emitChatSpans } from './chatSpans.js'; import { ATTR, - assistantOutputMessages, buildIntegrationAttrs, parseTimestamp, + recordOutput, setCompactionAttrs, } from './genaiSpans.js'; import type { CompactionAttrs } from './genaiSpans.js'; @@ -449,7 +449,6 @@ export class Session { const text = responses.flatMap(response => extractAssistantTextBlocks(response.content)); if (!text.length && options.lastMessage) text.push(options.lastMessage); const attributes: Attributes = {}; - if (text.length) attributes[ATTR.OUTPUT_MESSAGES] = assistantOutputMessages(text); const finishReasons = responses .map(response => response.finishReason) .filter((reason): reason is string => Boolean(reason)); @@ -457,8 +456,7 @@ export class Session { if (options.orphanReason) attributes[ATTR.WEAVE_ORPHAN_REASON] = options.orphanReason; if (Object.keys(attributes).length) turn.span.setAttributes(attributes); - const model = responses.filter(response => response.model).at(-1)?.model; - if (model) turn.span.record({ model }); + recordOutput(turn.span, text, responses.filter(response => response.model).at(-1)?.model); } private recordFinalTurnOutput( diff --git a/src/teamCoordinator.ts b/src/teamCoordinator.ts index be926ad..fc4be4a 100644 --- a/src/teamCoordinator.ts +++ b/src/teamCoordinator.ts @@ -7,7 +7,7 @@ import type * as weave from 'weave'; import { deferAgentOutcome, denyCall, finishAgentCall } from './callLifecycle.js'; import type { ToolResult, TracedAgent } from './callLifecycle.js'; import { emitChatSpans } from './chatSpans.js'; -import { ATTR, assistantOutputMessages, parseTimestamp } from './genaiSpans.js'; +import { ATTR, parseTimestamp, recordOutput } from './genaiSpans.js'; import type { ParsedTurn } from './parser.js'; import type { Session } from './session.js'; import { VERSION } from './setup.js'; @@ -111,9 +111,7 @@ function emitTeammate( }); try { emitChatSpans(span, responses, { agentName: memberName }); - const output = turns.flatMap(turn => turn.text); - if (output.length) span.setAttributes({ [ATTR.OUTPUT_MESSAGES]: assistantOutputMessages(output) }); - if (model) span.record({ model }); + recordOutput(span, turns.flatMap(turn => turn.text), model); return { model, text: turns.at(-1)?.text.join('\n') || undefined }; } finally { span.end({ endTime: parseTimestamp(responses.at(-1)?.endTime) ?? new Date() });