From 2bf8f9ec9714cbd33a0ecfb7b40dd7a335180fb7 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 5 Jun 2026 15:35:21 +0900 Subject: fix(observability): nest turn/step/prompt/provider.request spans into a tree (+ buildSpanOpen parent propagation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run-turn: step is now turnSpan.child; prompt/provider.request/tool-call are step's children (stepSpan.log passed into provider.stream). logger.ts: buildSpanOpen now propagates the child's computed parentSpanId onto the span-open record — a latent bug where span.child(...) never set parentSpanId on open (close was already correct). Verified: tsc -b clean, 279 tests, biome 0/0. Live: span tree turn->step->{prompt,provider.request}; the trace CLI easy-view renders the nesting. --- packages/kernel/src/runtime/run-turn.ts | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) (limited to 'packages/kernel/src/runtime/run-turn.ts') diff --git a/packages/kernel/src/runtime/run-turn.ts b/packages/kernel/src/runtime/run-turn.ts index 5e60641..01b280b 100644 --- a/packages/kernel/src/runtime/run-turn.ts +++ b/packages/kernel/src/runtime/run-turn.ts @@ -78,7 +78,7 @@ interface StepContext { readonly conversationId: string; readonly turnId: string; readonly logger: Logger; - readonly stepLogger: Logger | undefined; + readonly turnSpan: Span | undefined; readonly toolSpans: Map; } @@ -96,6 +96,7 @@ function processEvent( toolCalls: ToolCall[], dispatcher: StepDispatcher, ctx: StepContext, + stepSpan: Span | undefined, ): void { switch (event.type) { case "text-delta": @@ -129,12 +130,18 @@ function processEvent( ), ); - // Open a tool-call span (attrs: name, toolCallId) + // Open a tool-call span as a child of the step span (attrs: name, toolCallId) try { - const tcSpan = ctx.logger.span("tool-call", { - name: event.toolName, - toolCallId: event.toolCallId, - }); + const tcSpan = + stepSpan !== undefined + ? stepSpan.child("tool-call", { + name: event.toolName, + toolCallId: event.toolCallId, + }) + : ctx.logger.span("tool-call", { + name: event.toolName, + toolCallId: event.toolCallId, + }); ctx.toolSpans.set(event.toolCallId, tcSpan); } catch { // Swallow — D7: logging never breaks the turn. @@ -167,11 +174,12 @@ async function executeStep(ctx: StepContext): Promise { let stepUsage = zeroUsage(); let finishReason = "stop"; - // Open a step span; capture the verbatim pre-mutation prompt via a - // "prompt" child span whose body holds the serialized messages+tools. + // Open a step span as a child of the turn span; capture the verbatim + // pre-mutation prompt via a "prompt" child span whose body holds the + // serialized messages+tools. let stepSpan: Span | undefined; try { - stepSpan = ctx.logger.span("step"); + stepSpan = ctx.turnSpan !== undefined ? ctx.turnSpan.child("step") : ctx.logger.span("step"); const promptBody = JSON.stringify({ messages: ctx.messages, tools: ctx.tools }); const promptSpan = stepSpan.child( "prompt", @@ -198,12 +206,12 @@ async function executeStep(ctx: StepContext): Promise { try { const opts = { - ...(ctx.stepLogger !== undefined ? { logger: ctx.stepLogger } : {}), + ...(ctx.turnSpan !== undefined && stepSpan !== undefined ? { logger: stepSpan.log } : {}), }; const stream = ctx.provider.stream(ctx.messages, ctx.tools, opts); for await (const event of stream) { if (ctx.signal.aborted) break; - processEvent(event, chunks, toolCalls, dispatcher, ctx); + processEvent(event, chunks, toolCalls, dispatcher, ctx, stepSpan); if (event.type === "usage") { stepUsage = addUsage(stepUsage, event.usage); } @@ -354,7 +362,7 @@ export async function runTurn(input: RunTurnInput): Promise { conversationId, turnId, logger: turnSpan?.log ?? logger ?? createNoopLogger(), - stepLogger: logger, + turnSpan, toolSpans, }); -- cgit v1.2.3