diff --git a/src/agent/agent.ts b/src/agent/agent.ts index 928950b..58a1df3 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -32,7 +32,7 @@ import type { TokenUsage, ToolUseContext, } from '../types.js' -import { emitTrace } from '../utils/trace.js' +import { emitTrace, generateRunId } from '../utils/trace.js' import type { ToolDefinition as FrameworkToolDefinition, ToolRegistry } from '../tool/framework.js' import type { ToolExecutor } from '../tool/executor.js' import { createAdapter } from '../llm/adapter.js' @@ -275,7 +275,7 @@ export class Agent { ): Promise { this.transitionTo('running') - const agentStartMs = callerOptions?.onTrace ? Date.now() : 0 + const agentStartMs = Date.now() try { const runner = await this.getRunner() @@ -283,9 +283,12 @@ export class Agent { this.state.messages.push(msg) callerOptions?.onMessage?.(msg) } + // Auto-generate runId when onTrace is provided but runId is missing + const needsRunId = callerOptions?.onTrace && !callerOptions.runId const runOptions: RunOptions = { ...callerOptions, onMessage: internalOnMessage, + ...(needsRunId ? { runId: generateRunId() } : undefined), } const result = await runner.run(messages, runOptions) diff --git a/src/agent/pool.ts b/src/agent/pool.ts index aaf1fe3..aba0eb8 100644 --- a/src/agent/pool.ts +++ b/src/agent/pool.ts @@ -149,6 +149,7 @@ export class AgentPool { * * @param tasks - Array of `{ agent, prompt }` descriptors. */ + // TODO(#18): accept RunOptions per task to forward trace context async runParallel( tasks: ReadonlyArray<{ readonly agent: string; readonly prompt: string }>, ): Promise> { @@ -187,6 +188,7 @@ export class AgentPool { * * @throws {Error} If the pool is empty. */ + // TODO(#18): accept RunOptions to forward trace context async runAny(prompt: string): Promise { const allAgents = this.list() if (allAgents.length === 0) { diff --git a/src/agent/runner.ts b/src/agent/runner.ts index f2b0600..113f93c 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -78,8 +78,8 @@ export interface RunOptions { readonly onToolResult?: (name: string, result: ToolResult) => void /** Fired after each complete {@link LLMMessage} is appended. */ readonly onMessage?: (message: LLMMessage) => void - /** Trace callback for observability spans. */ - readonly onTrace?: (event: TraceEvent) => void + /** Trace callback for observability spans. Async callbacks are safe. */ + readonly onTrace?: (event: TraceEvent) => void | Promise /** Run ID for trace correlation. */ readonly runId?: string /** Task ID for trace correlation. */ @@ -264,16 +264,15 @@ export class AgentRunner { // ------------------------------------------------------------------ // Step 1: Call the LLM and collect the full response for this turn. // ------------------------------------------------------------------ - const llmStartMs = options.onTrace ? Date.now() : 0 + const llmStartMs = Date.now() const response = await this.adapter.chat(conversationMessages, baseChatOptions) if (options.onTrace) { const llmEndMs = Date.now() - const agentName = options.traceAgent ?? this.options.agentName ?? 'unknown' emitTrace(options.onTrace, { type: 'llm_call', runId: options.runId ?? '', taskId: options.taskId, - agent: agentName, + agent: options.traceAgent ?? this.options.agentName ?? 'unknown', model: this.options.model, turn: turns, tokens: response.usage, @@ -352,12 +351,11 @@ export class AgentRunner { options.onToolResult?.(block.name, result) if (options.onTrace) { - const agentName = options.traceAgent ?? this.options.agentName ?? 'unknown' emitTrace(options.onTrace, { type: 'tool_call', runId: options.runId ?? '', taskId: options.taskId, - agent: agentName, + agent: options.traceAgent ?? this.options.agentName ?? 'unknown', tool: block.name, isError: result.isError ?? false, startMs: startTime, diff --git a/src/index.ts b/src/index.ts index e849cd5..312f852 100644 --- a/src/index.ts +++ b/src/index.ts @@ -162,6 +162,7 @@ export type { OrchestratorEvent, // Trace + TraceEventType, TraceEventBase, TraceEvent, LLMCallTrace, @@ -174,4 +175,4 @@ export type { MemoryStore, } from './types.js' -export { emitTrace, generateRunId } from './utils/trace.js' +export { generateRunId } from './utils/trace.js' diff --git a/src/types.ts b/src/types.ts index 0f3ebff..418d54e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -315,19 +315,22 @@ export interface OrchestratorConfig { readonly defaultProvider?: 'anthropic' | 'copilot' | 'openai' readonly defaultBaseURL?: string readonly defaultApiKey?: string - onProgress?: (event: OrchestratorEvent) => void - onTrace?: (event: TraceEvent) => void + readonly onProgress?: (event: OrchestratorEvent) => void + readonly onTrace?: (event: TraceEvent) => void | Promise } // --------------------------------------------------------------------------- // Trace events — lightweight observability spans // --------------------------------------------------------------------------- +/** Trace event type discriminants. */ +export type TraceEventType = 'llm_call' | 'tool_call' | 'task' | 'agent' + /** Shared fields present on every trace event. */ export interface TraceEventBase { /** Unique identifier for the entire run (runTeam / runTasks / runAgent call). */ readonly runId: string - readonly type: string + readonly type: TraceEventType /** Unix epoch ms when the span started. */ readonly startMs: number /** Unix epoch ms when the span ended. */ @@ -343,7 +346,6 @@ export interface TraceEventBase { /** Emitted for each LLM API call (one per agent turn). */ export interface LLMCallTrace extends TraceEventBase { readonly type: 'llm_call' - readonly agent: string readonly model: string readonly turn: number readonly tokens: TokenUsage @@ -352,7 +354,6 @@ export interface LLMCallTrace extends TraceEventBase { /** Emitted for each tool execution. */ export interface ToolCallTrace extends TraceEventBase { readonly type: 'tool_call' - readonly agent: string readonly tool: string readonly isError: boolean } @@ -362,7 +363,6 @@ export interface TaskTrace extends TraceEventBase { readonly type: 'task' readonly taskId: string readonly taskTitle: string - readonly agent: string readonly success: boolean readonly retries: number } @@ -370,7 +370,6 @@ export interface TaskTrace extends TraceEventBase { /** Emitted when an agent run completes (wraps the full conversation loop). */ export interface AgentTrace extends TraceEventBase { readonly type: 'agent' - readonly agent: string readonly turns: number readonly tokens: TokenUsage readonly toolCalls: number diff --git a/src/utils/trace.ts b/src/utils/trace.ts index a238337..4f01f5f 100644 --- a/src/utils/trace.ts +++ b/src/utils/trace.ts @@ -10,7 +10,7 @@ import type { TraceEvent } from '../types.js' * subscriber never crashes agent execution. */ export function emitTrace( - fn: ((event: TraceEvent) => void) | undefined, + fn: ((event: TraceEvent) => void | Promise) | undefined, event: TraceEvent, ): void { if (!fn) return