Skip to content
Merged
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
7 changes: 7 additions & 0 deletions core/agent-tracing/src/ClaudeAgentTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Trace {
private startTime: number;
private executionOrder = 2; // Start at 2, root is 1
private pendingToolUses = new Map<string, Run>();
private outputMessages: Array<{ role: string; content: ClaudeContentBlock[] }> = [];
private tracer: ClaudeAgentTracer;

constructor(tracer: ClaudeAgentTracer, options?: CreateTraceOptions) {
Expand Down Expand Up @@ -87,6 +88,11 @@ class Trace {
const hasToolUse = content.some(c => c.type === 'tool_use');
const hasText = content.some(c => c.type === 'text');

// Collect assistant message for outputs.messages
if (content.length > 0) {
this.outputMessages.push({ role: 'assistant', content });
}

if (hasToolUse) {
const eventTime = Date.now();
// Create LLM run that initiated tool calls
Expand Down Expand Up @@ -164,6 +170,7 @@ class Trace {
// Update and log root run end
this.rootRun.end_time = this.startTime + (message.duration_ms || 0);
this.rootRun.outputs = {
messages: this.outputMessages,
result: message.result,
is_error: message.is_error,
num_turns: message.num_turns,
Expand Down
49 changes: 49 additions & 0 deletions core/agent-tracing/test/ClaudeAgentTracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,55 @@ describe('test/ClaudeAgentTracer.test.ts', () => {
});
});

describe('Trace outputs.messages in root run', () => {
it('should collect assistant text messages into outputs.messages', async () => {
const { claudeTracer, capturedRuns } = createTestEnv();
const trace = claudeTracer.createTrace();

const messages: SDKMessage[] = [
createMockInit(),
createMockAssistantWithTool(),
createMockUserToolResult(),
createMockAssistantTextOnly(),
createMockResult(),
];

for (const msg of messages) {
await trace.processMessage(msg);
}

const rootEnd = capturedRuns.find(e => !e.run.parent_run_id && e.status === RunStatus.END);
assert(rootEnd, 'Should have root_run end');
const outputMessages = (rootEnd.run.outputs as any)?.messages;
assert(Array.isArray(outputMessages), 'outputs.messages should be an array');
assert.strictEqual(outputMessages.length, 2);
// First message has text + tool_use
assert.strictEqual(outputMessages[0].role, 'assistant');
assert.strictEqual(outputMessages[0].content.length, 2);
assert.strictEqual(outputMessages[0].content[0].type, 'text');
assert.strictEqual(outputMessages[0].content[0].text, 'Let me run that command for you.');
assert.strictEqual(outputMessages[0].content[1].type, 'tool_use');
assert.strictEqual(outputMessages[0].content[1].name, 'Bash');
// Second message has text only
assert.strictEqual(outputMessages[1].role, 'assistant');
assert.deepStrictEqual(outputMessages[1].content, [{ type: 'text', text: 'The answer is 21.' }]);
});

it('should have empty messages array when no assistant text', async () => {
const { claudeTracer, capturedRuns } = createTestEnv();
const trace = claudeTracer.createTrace();

await trace.processMessage(createMockInit());
await trace.processMessage(createMockResult());

const rootEnd = capturedRuns.find(e => !e.run.parent_run_id && e.status === RunStatus.END);
assert(rootEnd, 'Should have root_run end');
const outputMessages = (rootEnd.run.outputs as any)?.messages;
assert(Array.isArray(outputMessages), 'outputs.messages should be an array');
assert.strictEqual(outputMessages.length, 0);
});
});

describe('Batch mode + text-only', () => {
it('should trace a text-only response via processMessages', async () => {
const { claudeTracer, capturedRuns } = createTestEnv();
Expand Down
Loading