Skip to content

Commit 820cfbb

Browse files
committed
feat: capture tool call responses in OTel spans
Hold tool spans open until the matching functionResponse event arrives, then record the result as gen_ai.tool.call.result. This makes tool outputs visible in Weave traces alongside the existing call arguments.
1 parent 5117a12 commit 820cfbb

File tree

1 file changed

+38
-10
lines changed

1 file changed

+38
-10
lines changed

src/agents/runner.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { InMemoryRunner, isFinalResponse } from '@google/adk';
1919
import type { LlmAgent } from '@google/adk';
2020
import type { Content } from '@google/genai';
21-
import type { Span as OtelSpan } from '@opentelemetry/api';
21+
import type { Context as OtelContext, Span as OtelSpan } from '@opentelemetry/api';
2222
import { SpanStatusCode, context, trace } from '@opentelemetry/api';
2323
import { agentLogger } from '../utils/logger.js';
2424
import { createPlanningAgent } from './planning.js';
@@ -141,14 +141,11 @@ function buildRetryMessage(
141141
}
142142

143143
/**
144-
* Record an OTel span for a tool call, nested under the current agent span.
144+
* Start an OTel span for a tool call, nested under the current agent span.
145+
* The caller is responsible for ending the span (after the tool response arrives).
145146
*/
146-
function recordToolSpan(
147-
name: string,
148-
args: unknown,
149-
parentCtx: ReturnType<typeof context.active>
150-
): void {
151-
const span = tracer.startSpan(
147+
function startToolSpan(name: string, args: unknown, parentCtx: OtelContext): OtelSpan {
148+
return tracer.startSpan(
152149
`tool: ${name}`,
153150
{
154151
attributes: {
@@ -159,7 +156,14 @@ function recordToolSpan(
159156
},
160157
parentCtx
161158
);
162-
span.end();
159+
}
160+
161+
/**
162+
* Truncate a serialized result string to a maximum length for span attributes.
163+
*/
164+
function truncateForSpan(value: string, maxLength = 4096): string {
165+
if (value.length <= maxLength) return value;
166+
return `${value.slice(0, maxLength)}...[truncated]`;
163167
}
164168

165169
const MAX_RETRIES = 5;
@@ -188,6 +192,7 @@ export async function* runAgent(
188192

189193
let currentAgentSpan: OtelSpan | null = null;
190194
let currentAgentCtx = rootCtx;
195+
const openToolSpans = new Map<string, OtelSpan>();
191196

192197
try {
193198
await getOrCreateSession(runner, sid);
@@ -268,7 +273,9 @@ export async function* runAgent(
268273
yield { type: 'text', content: part.text };
269274
}
270275
if ('functionCall' in part && part.functionCall?.name) {
271-
recordToolSpan(part.functionCall.name, part.functionCall.args, currentAgentCtx);
276+
const callId = part.functionCall.id ?? part.functionCall.name;
277+
const toolSpan = startToolSpan(part.functionCall.name, part.functionCall.args, currentAgentCtx);
278+
openToolSpans.set(callId, toolSpan);
272279
agentLogger.debug(
273280
`[Runner] Tool call: ${part.functionCall.name}, args: ${JSON.stringify(part.functionCall.args)}`
274281
);
@@ -282,6 +289,23 @@ export async function* runAgent(
282289
},
283290
};
284291
}
292+
if ('functionResponse' in part && part.functionResponse) {
293+
const { id, name, response } = part.functionResponse as {
294+
id?: string;
295+
name?: string;
296+
response?: unknown;
297+
};
298+
const callId = id ?? name;
299+
const toolSpan = callId ? openToolSpans.get(callId) : undefined;
300+
if (toolSpan) {
301+
toolSpan.setAttribute(
302+
'gen_ai.tool.call.result',
303+
truncateForSpan(JSON.stringify(response))
304+
);
305+
toolSpan.end();
306+
openToolSpans.delete(callId);
307+
}
308+
}
285309
}
286310
}
287311

@@ -334,6 +358,10 @@ export async function* runAgent(
334358
yield { type: 'done' };
335359
}
336360
} finally {
361+
for (const span of openToolSpans.values()) {
362+
span.end();
363+
}
364+
openToolSpans.clear();
337365
if (currentAgentSpan) currentAgentSpan.end();
338366
rootSpan.end();
339367
}

0 commit comments

Comments
 (0)