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
30 changes: 20 additions & 10 deletions core/agent-tracing/src/ClaudeAgentTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type ClaudeContentBlock,
type ClaudeTokenUsage,
type IRunCost,
type CreateSessionOptions,
RunStatus,
type TracerConfig,
applyTracerConfig,
Expand All @@ -23,16 +24,18 @@ import {
*/
export class TraceSession {
private traceId: string;
private threadId?: string;
private rootRun: Run | null = null;
private rootRunId: string;
private startTime: number;
private executionOrder = 2; // Start at 2, root is 1
private pendingToolUses = new Map<string, Run>();
private tracer: ClaudeAgentTracer;

constructor(tracer: ClaudeAgentTracer, sessionId?: string) {
constructor(tracer: ClaudeAgentTracer, options?: CreateSessionOptions) {
this.tracer = tracer;
this.traceId = sessionId || randomUUID();
this.traceId = options?.traceId || randomUUID();
this.threadId = options?.threadId;
this.rootRunId = randomUUID();
this.startTime = Date.now();
}
Expand Down Expand Up @@ -61,8 +64,11 @@ export class TraceSession {
}

private handleInit(message: ClaudeMessage): void {
this.traceId = message.session_id || this.traceId;
this.rootRun = this.tracer.createRootRunInternal(message, this.startTime, this.traceId, this.rootRunId);
// threadId: prefer constructor option, fallback to init message's session_id
if (!this.threadId) {
this.threadId = message.session_id;
}
this.rootRun = this.tracer.createRootRunInternal(message, this.startTime, this.traceId, this.rootRunId, this.threadId);
this.tracer.logTrace(this.rootRun, RunStatus.START);
}

Expand Down Expand Up @@ -213,14 +219,17 @@ export class ClaudeAgentTracer {
* Create a new trace session for streaming message processing.
* Use this for real-time tracing where messages arrive one-by-one.
*
* @param options.traceId - Server-side trace ID for call chain linking. Defaults to a random UUID.
* @param options.threadId - Thread ID (Claude SDK session ID), recorded in metadata.
*
* @example
* const session = claudeTracer.createSession();
* const session = claudeTracer.createSession({ traceId: ctx.tracer.traceId, threadId });
* for await (const message of agent.run('task')) {
* await session.processMessage(message);
* }
*/
public createSession(sessionId?: string): TraceSession {
return new TraceSession(this, sessionId);
public createSession(options?: CreateSessionOptions): TraceSession {
return new TraceSession(this, options);
}

/**
Expand Down Expand Up @@ -315,8 +324,9 @@ export class ClaudeAgentTracer {
* @internal
* Create root run from init message (used by TraceSession)
*/
createRootRunInternal(initMsg: ClaudeMessage, startTime: number, traceId: string, rootRunId?: string): Run {
createRootRunInternal(initMsg: ClaudeMessage, startTime: number, traceId: string, rootRunId?: string, threadId?: string): Run {
const runId = rootRunId || initMsg.uuid || randomUUID();
const resolvedThreadId = threadId || initMsg.session_id;

return {
id: runId,
Expand All @@ -325,7 +335,7 @@ export class ClaudeAgentTracer {
inputs: {
tools: initMsg.tools || [],
model: initMsg.model,
session_id: initMsg.session_id,
session_id: resolvedThreadId,
mcp_servers: initMsg.mcp_servers,
agents: initMsg.agents,
slash_commands: initMsg.slash_commands,
Expand All @@ -342,7 +352,7 @@ export class ClaudeAgentTracer {
tags: [],
extra: {
metadata: {
thread_id: initMsg.session_id,
thread_id: resolvedThreadId,
},
apiKeySource: initMsg.apiKeySource,
claude_code_version: initMsg.claude_code_version,
Expand Down
8 changes: 8 additions & 0 deletions core/agent-tracing/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ export const RunStatus = {
} as const;
export type RunStatus = (typeof RunStatus)[keyof typeof RunStatus];

/** Options for creating a new trace session. */
export interface CreateSessionOptions {
/** Server-side trace ID for linking to the request call chain. Defaults to a random UUID. */
traceId?: string;
/** Thread ID (Claude SDK session ID), recorded in metadata. */
threadId?: string;
}

/** User-facing config passed to tracer.configure() */
export interface TracerConfig {
agentName?: string;
Expand Down
51 changes: 49 additions & 2 deletions core/agent-tracing/test/ClaudeAgentTracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,10 @@ describe('test/ClaudeAgentTracer.test.ts', () => {
const toolEnd = toolRuns.find(e => e.status === RunStatus.END);
assert(toolEnd, 'Should have tool end');

// All runs share the same trace_id = session_id
// All runs share the same trace_id (auto-generated UUID, NOT session_id)
const traceIds = new Set(capturedRuns.map(e => e.run.trace_id));
assert.strictEqual(traceIds.size, 1, `All runs should share one trace_id, got ${traceIds.size}`);
assert.strictEqual([ ...traceIds ][0], 'test-session-001', 'trace_id should match session_id');
assert.notStrictEqual([ ...traceIds ][0], 'test-session-001', 'trace_id should NOT equal session_id');

// Root run should carry session_id as thread_id in extra.metadata
const rootExtra = rootStart.run.extra as Record<string, any>;
Expand All @@ -240,6 +240,53 @@ describe('test/ClaudeAgentTracer.test.ts', () => {
});
});

describe('Separate traceId and sessionId', () => {
it('should use provided traceId and record threadId in metadata', async () => {
const { claudeTracer, capturedRuns } = createTestEnv();
const session = claudeTracer.createSession({
traceId: 'server-trace-abc',
threadId: 'my-thread-id',
});

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

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

// All runs should use the provided traceId
const traceIds = new Set(capturedRuns.map(e => e.run.trace_id));
assert.strictEqual(traceIds.size, 1);
assert.strictEqual([ ...traceIds ][0], 'server-trace-abc', 'trace_id should be the server-side traceId');

// thread_id in metadata should be the sessionId, not the traceId
const rootStart = capturedRuns.find(e => !e.run.parent_run_id && e.status === RunStatus.START);
assert(rootStart);
const rootExtra = rootStart.run.extra as Record<string, any>;
assert.strictEqual(rootExtra?.metadata?.thread_id, 'my-thread-id', 'thread_id should be the provided threadId');
});

it('should fallback threadId to init message session_id when not provided', async () => {
const { claudeTracer, capturedRuns } = createTestEnv();
const session = claudeTracer.createSession({ traceId: 'server-trace-xyz' });

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

const rootStart = capturedRuns.find(e => !e.run.parent_run_id && e.status === RunStatus.START);
assert(rootStart);
const rootExtra = rootStart.run.extra as Record<string, any>;
assert.strictEqual(rootExtra?.metadata?.thread_id, 'test-session-001', 'thread_id should fallback to init session_id');

const traceIds = new Set(capturedRuns.map(e => e.run.trace_id));
assert.strictEqual([ ...traceIds ][0], 'server-trace-xyz');
});
});

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