fix: address second-round review findings

- Remove redundant `agent` re-declarations from trace subtypes
- Change TraceEventBase.type from `string` to literal union type
- Update onTrace callback type to `=> void | Promise<void>`
- Add readonly to onProgress/onTrace on OrchestratorConfig
- Remove Date.now() conditional guards (always capture timing)
- Auto-generate runId fallback when onTrace is set without runId
- Remove emitTrace from public API surface (keep generateRunId)
- Add TODO comments for runParallel()/runAny() trace forwarding
This commit is contained in:
JackChen 2026-04-03 15:24:41 +08:00
parent 8f7fc2019b
commit e696d877e7
6 changed files with 21 additions and 18 deletions

View File

@ -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<AgentRunResult> {
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)

View File

@ -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<Map<string, AgentRunResult>> {
@ -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<AgentRunResult> {
const allAgents = this.list()
if (allAgents.length === 0) {

View File

@ -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<void>
/** 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,

View File

@ -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'

View File

@ -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<void>
}
// ---------------------------------------------------------------------------
// 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

View File

@ -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<void>) | undefined,
event: TraceEvent,
): void {
if (!fn) return