Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/callLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
20 changes: 15 additions & 5 deletions src/genaiSpans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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. */
Expand Down
10 changes: 3 additions & 7 deletions src/hookHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -852,19 +852,15 @@ 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) {
if (transcript.model) {
recovered.span.setAttributes({ [ATTR.RESPONSE_MODEL]: transcript.model });
}
if (text) {
recovered.span.setAttributes({
[ATTR.OUTPUT_MESSAGES]: assistantOutputMessages([text]),
});
recordOutput(recovered.span, [text]);
}
}

Expand Down
6 changes: 2 additions & 4 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -449,16 +449,14 @@ 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));
if (finishReasons.length) attributes[ATTR.RESPONSE_FINISH_REASONS] = finishReasons;
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(
Expand Down
6 changes: 2 additions & 4 deletions src/teamCoordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() });
Expand Down
Loading