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 @@ -227,6 +227,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 separate package boundaries** — `mcp/server.ts` is the Stateless Worker wrapper; `mcp/client.ts` connects Agents to external servers; `mcp/index.ts` is a compatibility barrel for retained Legacy APIs whose implementation lives in `mcp/legacy-agent.ts`.
- **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 @@ -87,6 +87,7 @@ import {
type Observability,
type ObservabilityEvent
} from "./observability";
import { agentSpanAttributes } from "./observability/agent-span-attributes";
import { tracer } from "./observability/tracing/cloudflare";
import {
withInvocationScope,
Expand Down Expand Up @@ -1917,8 +1918,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
};
}
12 changes: 9 additions & 3 deletions packages/agents/src/observability/ai/wrapper/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 @@ -21,6 +22,7 @@ export function wrapModel(
model: unknown,
parentOperation: string,
storeMessages: boolean,
context: SemanticContext,
boundToInvocation = false
): unknown {
if (!wrapLanguageModel) {
Expand All @@ -44,7 +46,8 @@ export function wrapModel(
modelInfo,
params,
parentOperation,
storeMessages
storeMessages,
context
);
return tracer.withSpan(
span.name,
Expand Down Expand Up @@ -76,7 +79,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 @@ -126,13 +130,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,
model: model?.modelId,
operation,
provider: model?.provider,
Expand Down
31 changes: 28 additions & 3 deletions packages/agents/src/observability/ai/wrapper/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 @@ -11,6 +12,7 @@ export function wrapTools(
tracer: AgentTracer,
tools: unknown,
storeTools: boolean,
context: SemanticContext,
boundToInvocation = false,
approvedToolCalls?: Map<string, string>
): unknown {
Expand All @@ -27,6 +29,7 @@ export function wrapTools(
toolName,
tool,
storeTools,
context,
boundToInvocation,
approvedToolCalls
);
Expand All @@ -39,6 +42,7 @@ function wrapTool(
toolName: string,
tool: unknown,
storeTools: boolean,
context: SemanticContext,
boundToInvocation: boolean,
approvedToolCalls: Map<string, string> | undefined
): unknown {
Expand Down Expand Up @@ -69,6 +73,7 @@ function wrapTool(
toolRecord,
tool,
toolName,
context,
boundToInvocation
);
}
Expand All @@ -86,6 +91,7 @@ function wrapTool(

wrappedTool.execute = (...args) => {
const span = toolCallSpan({
context,
operation: "tool.execute",
toolCallId: extractToolCallId(args[1]),
toolName
Expand Down Expand Up @@ -116,6 +122,7 @@ function wrapTool(
toolName,
toolCallId,
"approved",
context,
boundToInvocation
);
if (toolCallId !== undefined) approvedToolCalls?.delete(toolCallId);
Expand Down Expand Up @@ -148,6 +155,7 @@ function wrapApprovalCheck(
toolRecord: Record<string, unknown>,
tool: object,
toolName: string,
context: SemanticContext,
boundToInvocation: boolean
): void {
const approval = toolRecord.needsApproval;
Expand All @@ -166,6 +174,7 @@ function wrapApprovalCheck(
toolName,
toolCallId,
"requested",
context,
boundToInvocation
);
}
Expand All @@ -183,6 +192,7 @@ export function wrapToolApprovalPolicy(
tracer: AgentTracer,
policy: unknown,
approvedToolCalls: Map<string, string>,
context: SemanticContext,
boundToInvocation = false
): unknown {
if (typeof policy === "function") {
Expand All @@ -195,6 +205,7 @@ export function wrapToolApprovalPolicy(
readString(toolCall?.toolCallId),
tracer,
approvedToolCalls,
context,
boundToInvocation
);
};
Expand All @@ -212,6 +223,7 @@ export function wrapToolApprovalPolicy(
extractToolCallId(args[1]),
tracer,
approvedToolCalls,
context,
boundToInvocation
)
]
Expand All @@ -225,6 +237,7 @@ function observePolicyResult(
toolCallId: string | undefined,
tracer: AgentTracer,
approvedToolCalls: Map<string, string>,
context: SemanticContext,
boundToInvocation = false
): unknown {
const observe = (status: unknown): unknown => {
Expand All @@ -241,6 +254,7 @@ function observePolicyResult(
toolName,
toolCallId,
type === "denied" ? "denied" : "requested",
context,
boundToInvocation
);
}
Expand All @@ -254,15 +268,17 @@ function observePolicyResult(
/** 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 @@ -273,9 +289,11 @@ function recordApprovalSegment(
toolName: string,
toolCallId: string | undefined,
state: "approved" | "denied" | "requested",
context: SemanticContext,
boundToInvocation = false
): void {
const tool = toolCallSpan({
context,
operation: "tool.approval",
toolCallId,
toolName
Expand All @@ -289,6 +307,7 @@ function recordApprovalSegment(
toolName,
toolCallId,
state,
context,
boundToInvocation
);
},
Expand All @@ -301,9 +320,15 @@ function recordApprovalChild(
toolName: string,
toolCallId: string | undefined,
state: "approved" | "denied" | "requested",
context: SemanticContext,
boundToInvocation = false
): void {
const approval = toolApprovalSpan({ state, toolCallId, toolName });
const approval = toolApprovalSpan({
context,
state,
toolCallId,
toolName
});
tracer.withSpan(
approval.name,
approval.attributes,
Expand Down
17 changes: 15 additions & 2 deletions packages/agents/src/observability/ai/wrapper/wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ function createOperationWrapper(
return operation(params, ...args);
}

const context = semanticContext(params);
const span = operationSpanForCall(
operationName,
extractModelInfo(params.model),
Expand All @@ -166,7 +167,8 @@ function createOperationWrapper(
writeSpanAttributes(operationSpan, span.attributes);
recordDeniedApprovalResponses(
instrumentation.tracer,
params.messages
params.messages,
context
);

const startedAtMs = Date.now();
Expand All @@ -177,6 +179,7 @@ function createOperationWrapper(
wrapLanguageModel,
instrumentation.tracer,
storage,
context,
boundToInvocation
),
...args
Expand All @@ -203,14 +206,19 @@ function createOperationWrapper(
return operation(params, ...args);
}

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

const result = await operation(
operationParamsForCall(
Expand All @@ -219,6 +227,7 @@ function createOperationWrapper(
wrapLanguageModel,
instrumentation.tracer,
storage,
context,
boundToInvocation
),
...args
Expand Down Expand Up @@ -260,6 +269,7 @@ function operationParamsForCall(
wrapLanguageModel: AISDKWrapLanguageModel | undefined,
tracer: AgentTracer,
storage: ResolvedAISDKStorageOptions,
context: SemanticContext,
boundToInvocation = false
): AISDKCallParams {
const approvedToolCalls = new Map<string, string>();
Expand All @@ -271,6 +281,7 @@ function operationParamsForCall(
tracer,
params.tools,
storage.storeTools,
context,
boundToInvocation,
approvedToolCalls
)
Expand All @@ -282,6 +293,7 @@ function operationParamsForCall(
tracer,
params.toolApproval,
approvedToolCalls,
context,
boundToInvocation
)
}
Expand All @@ -294,6 +306,7 @@ function operationParamsForCall(
params.model,
operationName,
storage.storeMessages,
context,
boundToInvocation
)
}
Expand Down
Loading
Loading