Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/curly-agents-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": patch
---

Add Agents SDK instrumentation and agent instance identity attributes to SDK-created spans.
1 change: 1 addition & 0 deletions packages/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ AI evaluation suite (scheduling accuracy, etc.). Requires API keys in `.env`.
- **Sub-agents are facets** — `subAgent(Cls, name)` creates or resolves a child DO colocated on the same machine. Clients reach a child via `/agents/{parent}/{name}/sub/{child}/{name}` and `useAgent({ sub: [...] })`. Parents gate access with `onBeforeSubAgent`; children reach their parent with `parentAgent(Cls)` or `parentPath`.
- **Scheduling uses cron-schedule** — `this.schedule()` accepts delays, Dates, or cron strings. Schedules persist in SQLite and survive hibernation.
- **MCP has two sides** — `McpAgent` (in `mcp/index.ts`) lets you _build_ an MCP server. `MCPClientManager` (in `mcp/client.ts`) lets an Agent _connect to_ external MCP servers.
- **Telemetry has an independent schema version** — `instrumentation_scope.version` is hardcoded to `"1"`; it is not the package version. Notify Workers Observability and any other downstream consumers before bumping it.

## Boundaries

Expand Down
8 changes: 6 additions & 2 deletions packages/agents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
type Observability,
type ObservabilityEvent
} from "./observability";
import { agentSpanAttributes } from "./observability/agent-span-attributes";
import { tracer } from "./observability/tracing/cloudflare";
import {
writeSpanAttributes,
Expand Down Expand Up @@ -1890,8 +1891,11 @@ export class Agent<
return tracer.withSpan(
operation,
{
"cloudflare.agents.agent.id": agentId,
"cloudflare.agents.agent.name": this._ParentClass.name,
...agentSpanAttributes({
agentClassName: this._ParentClass.name,
sessionId: this.ctx.id.toString(),
sessionName: agentId
}),
"cloudflare.agents.operation.name": operation,
"cloudflare.agents.storage.grouped": true,
"cloudflare.agents.storage.system": "durable_object",
Expand Down
19 changes: 19 additions & 0 deletions packages/agents/src/observability/agent-span-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { TraceAttributes } from "./tracing/tracer";

export function agentSpanAttributes(input: {
readonly agentClassName: string;
readonly sessionId: string;
readonly sessionName: string | undefined;
}): TraceAttributes {
return {
// Compatibility representation of InstrumentationScope until the Workers
// tracing API exposes native scope metadata.
"instrumentation_scope.name": "agents",
// Before bumping this telemetry schema version, notify Workers
// Observability and any other downstream consumers.
"instrumentation_scope.version": "1",
"cloudflare.agents.session.id": input.sessionId,
"cloudflare.agents.session.name": input.sessionName,
"gen_ai.agent.name": input.agentClassName
};
}
14 changes: 10 additions & 4 deletions packages/agents/src/observability/ai/v6/model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { aiGatewayLogAttributes, modelCallSpan } from "../../genai/telemetry";
import type { SemanticContext } from "../../genai/telemetry";
import { writeSpanAttributes } from "../../tracing/tracer";
import type { AgentSpan, AgentTracer } from "../../tracing/tracer";
import {
Expand All @@ -20,7 +21,8 @@ export function wrapModel(
wrapLanguageModel: AISDKV6WrapLanguageModel | undefined,
model: unknown,
parentOperation: string,
storeMessages: boolean
storeMessages: boolean,
context: SemanticContext
): unknown {
if (!wrapLanguageModel) {
return model;
Expand All @@ -43,7 +45,8 @@ export function wrapModel(
modelInfo,
params,
parentOperation,
storeMessages
storeMessages,
context
);
return tracer.withSpan(
span.name,
Expand Down Expand Up @@ -74,7 +77,8 @@ export function wrapModel(
modelInfo,
params,
parentOperation,
storeMessages
storeMessages,
context
);
// The provider call runs INSIDE the activation callback so its work
// (fetch subrequests, etc.) nests under the chat span; the span stays
Expand Down Expand Up @@ -123,13 +127,15 @@ function modelCallSpanForModel(
model: ModelInfo | undefined,
params: unknown,
parentOperation: string,
storeMessages: boolean
storeMessages: boolean,
context: SemanticContext
): ReturnType<typeof modelCallSpan> {
const record =
typeof params === "object" && params !== null
? (params as Record<string, unknown>)
: {};
const span = modelCallSpan({
context,
integration: "ai-sdk",
model: model?.modelId,
operation,
Expand Down
59 changes: 46 additions & 13 deletions packages/agents/src/observability/ai/v6/tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { toolApprovalSpan, toolCallSpan } from "../../genai/telemetry";
import type { SemanticContext } from "../../genai/telemetry";
import { toolInputAttributes, toolOutputAttributes } from "../content";
import { readString } from "../read";
import type { AgentSpan, AgentTracer } from "../../tracing/tracer";
Expand All @@ -10,7 +11,8 @@ type ContextSnapshot = <R>(fn: () => R) => R;
export function wrapTools(
tracer: AgentTracer,
tools: unknown,
storeTools: boolean
storeTools: boolean,
context: SemanticContext
): unknown {
if (typeof tools !== "object" || tools === null) {
return tools;
Expand All @@ -20,7 +22,13 @@ export function wrapTools(
const toolRecord = tools as Record<string, unknown>;
const wrappedTools: Record<string, unknown> = {};
for (const [toolName, tool] of Object.entries(toolRecord)) {
wrappedTools[toolName] = wrapTool(tracer, toolName, tool, storeTools);
wrappedTools[toolName] = wrapTool(
tracer,
toolName,
tool,
storeTools,
context
);
}
return wrappedTools;
}
Expand All @@ -29,7 +37,8 @@ function wrapTool(
tracer: AgentTracer,
toolName: string,
tool: unknown,
storeTools: boolean
storeTools: boolean,
context: SemanticContext
): unknown {
if (typeof tool !== "object" || tool === null) {
return tool;
Expand All @@ -52,7 +61,7 @@ function wrapTool(
execute: (...args: readonly unknown[]) => unknown;
};
if (hasApproval) {
wrapApprovalCheck(tracer, wrappedTool, toolRecord, tool, toolName);
wrapApprovalCheck(tracer, wrappedTool, toolRecord, tool, toolName, context);
}
if (!hasExecute) {
return wrappedTool;
Expand All @@ -68,6 +77,7 @@ function wrapTool(

wrappedTool.execute = (...args) => {
const span = toolCallSpan({
context,
integration: "ai-sdk",
operation: "tool.execute",
toolCallId: extractToolCallId(args[1]),
Expand All @@ -89,7 +99,13 @@ function wrapTool(
extractToolCallId(args[1])
);
if (approval?.approved === true) {
recordApprovalChild(tracer, toolName, approval.toolCallId, "approved");
recordApprovalChild(
tracer,
toolName,
approval.toolCallId,
"approved",
context
);
}
const result = originalExecute(...args);

Expand All @@ -116,7 +132,8 @@ function wrapApprovalCheck(
wrappedTool: Record<string, unknown>,
toolRecord: Record<string, unknown>,
tool: object,
toolName: string
toolName: string,
context: SemanticContext
): void {
const approval = toolRecord.needsApproval;
const original =
Expand All @@ -129,7 +146,13 @@ function wrapApprovalCheck(
const recordRequested = (needed: unknown): unknown => {
const toolCallId = extractToolCallId(args[1]);
if (needed === true && !hasApprovalResponse(args[1], toolCallId)) {
recordApprovalSegment(tracer, toolName, toolCallId, "requested");
recordApprovalSegment(
tracer,
toolName,
toolCallId,
"requested",
context
);
}
return needed;
};
Expand All @@ -143,15 +166,17 @@ function wrapApprovalCheck(
/** Records denied responses, whose tool never reaches execute(). */
export function recordDeniedApprovalResponses(
tracer: AgentTracer,
messages: unknown
messages: unknown,
context: SemanticContext
): void {
for (const response of approvalResponses(messages)) {
if (!response.approved) {
recordApprovalSegment(
tracer,
response.toolName,
response.toolCallId,
"denied"
"denied",
context
);
}
}
Expand All @@ -161,26 +186,34 @@ function recordApprovalSegment(
tracer: AgentTracer,
toolName: string,
toolCallId: string | undefined,
state: "approved" | "denied" | "requested"
state: "approved" | "denied" | "requested",
context: SemanticContext
): void {
const tool = toolCallSpan({
context,
integration: "ai-sdk",
operation: "tool.approval",
toolCallId,
toolName
});
tracer.withSpan(tool.name, tool.attributes, () => {
recordApprovalChild(tracer, toolName, toolCallId, state);
recordApprovalChild(tracer, toolName, toolCallId, state, context);
});
}

function recordApprovalChild(
tracer: AgentTracer,
toolName: string,
toolCallId: string | undefined,
state: "approved" | "denied" | "requested"
state: "approved" | "denied" | "requested",
context: SemanticContext
): void {
const approval = toolApprovalSpan({ state, toolCallId, toolName });
const approval = toolApprovalSpan({
context,
state,
toolCallId,
toolName
});
tracer.withSpan(approval.name, approval.attributes, () => undefined);
}

Expand Down
38 changes: 27 additions & 11 deletions packages/agents/src/observability/ai/v6/wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,19 @@ function createOperationWrapper(
return operation(params, ...args);
}

const context = semanticContext(params);
const span = operationSpanForCall(
operationName,
extractModelInfo(params.model),
params,
instrumentation.options
instrumentation.options,
context
);
writeSpanAttributes(operationSpan, span.attributes);
recordDeniedApprovalResponses(
instrumentation.tracer,
params.messages
params.messages,
context
);

const startedAtMs = Date.now();
Expand All @@ -168,7 +171,8 @@ function createOperationWrapper(
operationName,
wrapLanguageModel,
instrumentation.tracer,
storage
storage,
context
),
...args
);
Expand All @@ -192,22 +196,29 @@ function createOperationWrapper(
return operation(params, ...args);
}

const context = semanticContext(params);
const span = operationSpanForCall(
operationName,
extractModelInfo(params.model),
params,
instrumentation.options
instrumentation.options,
context
);
writeSpanAttributes(operationSpan, span.attributes);
recordDeniedApprovalResponses(instrumentation.tracer, params.messages);
recordDeniedApprovalResponses(
instrumentation.tracer,
params.messages,
context
);

const result = await operation(
operationParamsForCall(
params,
operationName,
wrapLanguageModel,
instrumentation.tracer,
storage
storage,
context
),
...args
);
Expand Down Expand Up @@ -250,12 +261,15 @@ function operationParamsForCall(
operationName: AISDKV6OperationName,
wrapLanguageModel: AISDKV6WrapLanguageModel | undefined,
tracer: AgentTracer,
storage: ResolvedAISDKStorageOptions
storage: ResolvedAISDKStorageOptions,
context: SemanticContext
): AISDKV6CallParams {
return {
...params,
...(shouldWrapTools(operationName) && params.tools !== undefined
? { tools: wrapTools(tracer, params.tools, storage.storeTools) }
? {
tools: wrapTools(tracer, params.tools, storage.storeTools, context)
}
: {}),
...(params.model !== undefined
? {
Expand All @@ -264,7 +278,8 @@ function operationParamsForCall(
wrapLanguageModel,
params.model,
operationName,
storage.storeMessages
storage.storeMessages,
context
)
}
: {})
Expand Down Expand Up @@ -305,14 +320,15 @@ function operationSpanForCall(
operation: string,
model: ModelInfo | undefined,
params: AISDKV6CallParams,
options: AISDKInstrumentationOptions | undefined
options: AISDKInstrumentationOptions | undefined,
context: SemanticContext
): ReturnType<typeof operationSpan> {
return operationSpan({
attributes: {
...metadataAttributes(telemetryMetadata(params)),
...contextAttributes(params, options)
},
context: semanticContext(params),
context,
integration: "ai-sdk",
model: model?.modelId,
operation,
Expand Down
Loading
Loading