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
6 changes: 6 additions & 0 deletions .changeset/lucky-traces-nest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"agents": patch
"@cloudflare/think": patch
---

Keep AI SDK v7 `chat` and `execute_tool` spans under their `invoke_agent` operation span, and record requested, approved, and denied `tool_approval` segments under `execute_tool`. The v7 operation wrapper establishes parent context before the SDK starts model or tool work without restoring unrelated `AsyncLocalStorage` state such as provider or tool authorization.
64 changes: 21 additions & 43 deletions docs/agents/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ metadata fields for applications with a different identity model. Payload
storage is off by default. Set `storeMessages` and/or `storeTools` on the Think
agent to opt in; these are wrapper settings, not span attributes.

### AI SDK v6
### AI SDK v6 and v7

Wrap the SDK namespace:

Expand All @@ -304,13 +304,17 @@ const { generateText, streamText } = wrapAISDK(ai);
```

`wrapAISDK` instruments `generateText`, `streamText`, `generateObject`, and
`streamObject`. Span names use `{operation} {target}` and fall back to the bare
operation past 64 UTF-8 bytes; the full target remains on its semantic
attribute. A model object is wrapped with the SDK's `wrapLanguageModel` helper,
so provider work is a `chat {model}` child of the operation span. Tool
execution is wrapped as `execute_tool {tool}`. AI SDK v6 approval lifecycle
segments appear as bounded `tool_approval {tool}` children of an
`execute_tool {tool}` span, correlated by `gen_ai.tool.call.id` and carrying
`streamObject` on both supported AI SDK versions. With v7, the wrapper starts
`invoke_agent` before the SDK captures its asynchronous operation context. This
keeps every `chat` and `execute_tool` span under that operation without
restoring unrelated `AsyncLocalStorage` state used by providers or tools.

Span names use `{operation} {target}` and fall back to the bare operation past
64 UTF-8 bytes; the full target remains on its semantic attribute. Provider
work runs under `chat {model}`, and tool work runs under
`execute_tool {tool}`. AI SDK v6 and v7 approval lifecycle segments appear as
bounded `tool_approval {tool}` children of `execute_tool {tool}`, correlated by
`gen_ai.tool.call.id` and carrying
`cloudflare.agents.tool.approval.state` (`requested`, `approved`, or `denied`).
They never remain open while waiting for a human across invocations. Stream
spans close on completion, cancellation, an in-band error, or early consumer
Expand All @@ -329,41 +333,15 @@ const traced = wrapAISDK(ai, {
});
```

### AI SDK v7
### AI SDK v7 lifecycle adapter

AI SDK v7 ships a first-class telemetry lifecycle. Register the adapter once and
every `generateText`, `streamText`, `generateObject`, and `streamObject` call is
instrumented:

```ts
import { registerTelemetry } from "ai";
import { createAISDKTelemetry } from "agents/observability/ai";

registerTelemetry(
createAISDKTelemetry({ storeMessages: true, storeTools: true })
);
```

Or scope it to a single call through `experimental_telemetry`:

```ts
import { createAISDKTelemetry } from "agents/observability/ai";

await generateText({
model,
prompt: "...",
experimental_telemetry: {
integrations: [createAISDKTelemetry()]
}
});
```

The v7 adapter uses `cloudflare.agents.call.id` to correlate operation, model,
and tool spans. Its execution hooks keep provider work under the `chat` span and
nested work performed by a tool under the `execute_tool` span. It handles both
`onEnd` and `onAbort` terminal paths. `wrapAISDK` and `createAISDKTelemetry`
project into the same span schema, so the two SDK versions are dashboard-
compatible.
`createAISDKTelemetry` remains available for applications that need a global or
per-call AI SDK v7 lifecycle integration. It uses
`cloudflare.agents.call.id` to correlate operation, model, and tool spans, and
its execution hooks put provider work under `chat` and nested tool work under
`execute_tool`. The lifecycle API does not keep its operation callback active
for later model and tool execution, so use `wrapAISDK` when the
`invoke_agent` parent-child hierarchy is required.

When a gateway-backed provider exposes its AI Gateway log ID through response
headers, provider metadata, or the Workers AI binding, the corresponding
Expand Down Expand Up @@ -445,7 +423,7 @@ await generateText({
| `gen_ai.usage.cache_creation.input_tokens`, `gen_ai.usage.cache_read.input_tokens` | Provider cache usage when reported |
| `gen_ai.usage.reasoning.output_tokens` | Reasoning output usage when reported |
| `gen_ai.tool.name`, `gen_ai.tool.type`, `gen_ai.tool.call.id` | Tool identity; call ID also correlates approval lifecycle segments |
| `cloudflare.agents.tool.approval.state` | AI SDK v6 approval lifecycle segment: `requested`, `approved`, or `denied` |
| `cloudflare.agents.tool.approval.state` | AI SDK approval lifecycle segment: `requested`, `approved`, or `denied` |
| `gen_ai.input.messages`, `gen_ai.output.messages` | Opt-in OTel-schema model messages on `chat`, including tool parts and output finish reasons |
| `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result` | Opt-in tool arguments/results on `execute_tool` |
| `user.id` | Explicit v6 metadata key `user.id` |
Expand Down
14 changes: 8 additions & 6 deletions packages/agents/src/observability/ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
} from "./options";
import { createAISDKV6Wrapper } from "./v6/wrap";
import { createAISDKV7Telemetry } from "./v7/telemetry";
import { createAISDKV7Wrapper } from "./v7/wrap";
import type { AISDKV7Telemetry } from "./v7/types";
import { tracer } from "../tracing/cloudflare";

Expand All @@ -18,15 +19,16 @@ export function wrapAISDK<T extends Record<string, unknown>>(
ai: T,
options: AISDKInstrumentationOptions = {}
): T {
return createAISDKV6Wrapper(ai, {
options,
tracer
});
const instrumentation = { options, tracer };
return typeof ai.registerTelemetry === "function"
? createAISDKV7Wrapper(ai, instrumentation)
: createAISDKV6Wrapper(ai, instrumentation);
}

/**
* Creates an AI SDK v7 telemetry adapter for use with `registerTelemetry` or
* per-call telemetry configuration.
* Creates an AI SDK v7 lifecycle adapter for use with `registerTelemetry` or
* per-call telemetry configuration. Use {@link wrapAISDK} when model and tool
* spans must be children of the `invoke_agent` operation span.
*/
export function createAISDKTelemetry(
options: AISDKStorageOptions = {}
Expand Down
4 changes: 2 additions & 2 deletions packages/agents/src/observability/ai/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export type ResolvedAISDKStorageOptions = {
readonly storeTools: boolean;
};

/** Instrumentation options for the AI SDK v6 adapter. */
/** Instrumentation options for the AI SDK namespace wrapper. */
export type AISDKInstrumentationOptions = AISDKStorageOptions & {
/** AI SDK v6 `experimental_context` keys to emit as scalar attributes. */
/** Runtime-context keys to emit as scalar attributes. */
readonly includeRuntimeContext?: readonly string[];
};
Loading
Loading