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

Reduce SDK trace noise and use consistent verb-object names for agent lifecycle, request handling, turns, submissions, and response streaming.
48 changes: 29 additions & 19 deletions docs/agents/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,17 +260,22 @@ These events are emitted by `AIChatAgent` from `@cloudflare/ai-chat`. They track
| `email:receive` | `{ from, to, subject? }` | An email is received |
| `email:reply` | `{ from, to, subject? }` | A reply email is sent |

## Agent initialization span

When Worker traces are enabled, every `Agent` constructor runs its setup —
method wrapping, schema creation, and MCP client manager initialization —
inside an `agent_initialization` span. Constructor-time child spans group under
this one stable parent instead of appearing as top-level clutter. The span
carries `cloudflare.agents.agent.name` (the agent class),
`cloudflare.agents.agent.id` (the named instance, omitted when the name is not
yet readable during construction), and `cloudflare.agents.operation.name`
(`agent_initialization`). Like the rest of the tracing in this package, it is a
no-op when the runtime has no native tracing capability.
## Agent lifecycle spans

When Worker traces are enabled, Agent lifecycle work is grouped under stable
verb-object span names:

- `initialize_agent` covers base constructor setup.
- `initialize_chat_agent` covers AIChat constructor setup.
- `start_agent` covers base startup and recovery.
- `start_agent_runtime` covers Think runtime and session startup.

Internal setup steps do not emit additional spans. Each lifecycle span carries
`cloudflare.agents.agent.name` (the agent class),
`cloudflare.agents.agent.id` (the named instance), and
`cloudflare.agents.operation.name`. Like the rest of the tracing in this
package, lifecycle tracing is a no-op when the runtime has no native tracing
capability.

## AI SDK tracing

Expand All @@ -283,14 +288,19 @@ integration is a no-op when the runtime has no native tracing capability.

**Think agents are traced out of the box.** Enable
`observability.traces.enabled` in `wrangler.jsonc`; no Think option is required.
A turn gets an `invoke_agent {agent class}` operation span, `chat {model}` model
spans, and `execute_tool {tool}` spans. Think always supplies its durable
identity: `gen_ai.agent.name` is the class name, `gen_ai.agent.id` is the named
instance, and `gen_ai.conversation.id` is the opaque Durable Object ID. These
are defaults; `beforeTurn` can override `functionId` or the corresponding
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.
A chat request is handled under `handle_chat_request`, and each admitted turn
runs under `run_agent_turn`. The turn contains `invoke_agent {agent class}`
operation spans, `chat {model}` model spans, `execute_tool {tool}` spans, and a
`stream_agent_response` span while the response is consumed, broadcast, and
persisted. Durable turns use `submit_agent_turn` when accepted and
`run_submitted_agent_turn` when later executed. Think always supplies its
durable identity: `gen_ai.agent.name` is the class name,
`gen_ai.agent.id` is the named instance, and `gen_ai.conversation.id` is the
opaque Durable Object ID. These are defaults; `beforeTurn` can override
`functionId` or the corresponding 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 and v7

Expand Down
197 changes: 52 additions & 145 deletions packages/agents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1958,14 +1958,6 @@ export class Agent<
throw new SqlError(query, e);
}
}
private _schemaInitialization:
| {
previousVersion: number;
currentVersion: number;
migrated: boolean;
}
| undefined;

/**
* Create all internal tables and run migrations if needed.
* Called by the constructor on every wake. Idempotent — skips DDL when
Expand Down Expand Up @@ -2324,46 +2316,23 @@ export class Agent<
VALUES (${SCHEMA_VERSION_ROW_ID}, ${String(CURRENT_SCHEMA_VERSION)})
`;
}

this._schemaInitialization = {
previousVersion: schemaVersion,
currentVersion: CURRENT_SCHEMA_VERSION,
migrated: schemaVersion < CURRENT_SCHEMA_VERSION
};
}

constructor(ctx: AgentContext, env: Env) {
super(ctx, env);

this.mcp = this._withAgentSpan(
"agent_initialization",
"initialize_agent",
"initialization",
{},
(update) => {
() => {
if (!wrappedClasses.has(this.constructor)) {
// Auto-wrap custom methods with agent context
this._autoWrapCustomMethods();
wrappedClasses.add(this.constructor);
}

this._withAgentSpan(
"initialize_agent_storage",
"initialization",
{},
(updateStorage) => {
this._ensureSchema();
const schemaAttributes = {
"cloudflare.agents.schema.version.previous":
this._schemaInitialization?.previousVersion,
"cloudflare.agents.schema.version.current":
this._schemaInitialization?.currentVersion,
"cloudflare.agents.schema.migrated":
this._schemaInitialization?.migrated
};
updateStorage(schemaAttributes);
update(schemaAttributes);
}
);
this._ensureSchema();

// Initialize MCPClientManager AFTER tables are created
return new MCPClientManager(this._ParentClass.name, "0.0.1", {
Expand Down Expand Up @@ -2731,67 +2700,46 @@ export class Agent<
email: undefined
},
async () => {
await this._withAgentSpan(
"restore_agent_state",
"startup",
{},
async () => {
// Hydrate _isFacet from persistent storage so the flag
// survives hibernation (the DO constructor resets it to false).
const isFacet =
await this.ctx.storage.get<boolean>("cf_agents_is_facet");
if (isFacet) this._isFacet = true;

const storedFacetName = await this.ctx.storage.get<string>(
"cf_agents_facet_name"
);
if (typeof storedFacetName === "string") {
this._facetName = storedFacetName;
}

const storedParentPath = await this.ctx.storage.get<
Array<{ className: string; name: string }>
>("cf_agents_parent_path");
if (isValidParentPath(storedParentPath)) {
this._parentPath = storedParentPath;
}
try {
await this._cf_hydrateSubAgentConnectionsFromRoot();
} catch (error) {
console.warn(
"[Agent] Unable to hydrate sub-agent WebSocket connections:",
error
);
}
}
// Hydrate _isFacet from persistent storage so the flag survives
// hibernation (the DO constructor resets it to false).
const isFacet =
await this.ctx.storage.get<boolean>("cf_agents_is_facet");
if (isFacet) this._isFacet = true;

const storedFacetName = await this.ctx.storage.get<string>(
"cf_agents_facet_name"
);
if (typeof storedFacetName === "string") {
this._facetName = storedFacetName;
}

const storedParentPath = await this.ctx.storage.get<
Array<{ className: string; name: string }>
>("cf_agents_parent_path");
if (isValidParentPath(storedParentPath)) {
this._parentPath = storedParentPath;
}
try {
await this._cf_hydrateSubAgentConnectionsFromRoot();
} catch (error) {
console.warn(
"[Agent] Unable to hydrate sub-agent WebSocket connections:",
error
);
}

await this._tryCatch(async () => {
// Restore MCP connections before fiber/chat recovery so recovered
// turns see MCP tools. Restored connections re-advertise the
// capabilities persisted from the previous session; the handlers
// behind them attach when onStart() configures them.
await this._withAgentSpan(
"restore_mcp_connections",
"startup",
{},
async () => {
await this.mcp.restoreConnectionsFromStorage(this.name);
await this._restoreRpcMcpServers();
this.broadcastMcpServers();
}
);
await this.mcp.restoreConnectionsFromStorage(this.name);
await this._restoreRpcMcpServers();
this.broadcastMcpServers();

const startupAgentToolRunIds = await this._withAgentSpan(
"recover_agent_work",
"startup",
{},
async () => {
this._checkOrphanedWorkflows();
await this._checkRunFibers();
return this._agentToolRunRecoveryRunIds();
}
);
this._checkOrphanedWorkflows();
await this._checkRunFibers();
const startupAgentToolRunIds = this._agentToolRunRecoveryRunIds();
update({
"cloudflare.agents.start.facet": this._isFacet,
"cloudflare.agents.recovery.agent_tools.count":
Expand All @@ -2812,12 +2760,7 @@ export class Agent<
this._warnedScheduleInOnStart.clear();
let result: Awaited<ReturnType<typeof _onStart>>;
try {
result = await this._withAgentSpan(
"run_user_on_start",
"startup",
{},
() => _onStart(props)
);
result = await _onStart(props);
} finally {
this._insideOnStart = false;
}
Expand Down Expand Up @@ -2859,7 +2802,7 @@ export class Agent<
);
};
this.onStart = (props?: Props) =>
this._withAgentSpan("agent_start", "startup", {}, (update) =>
this._withAgentSpan("start_agent", "startup", {}, (update) =>
startAgent(props, update)
);
}
Expand Down Expand Up @@ -5656,20 +5599,10 @@ export class Agent<
options?: InternalFiberOptions
): Promise<T> {
const signal = options?.signal ?? new AbortController().signal;
this._withAgentSpan(
"initialize_fiber",
"fiber",
{
"cloudflare.agents.fiber.id": id,
"cloudflare.agents.fiber.name": name
},
() => {
this.sql`
INSERT INTO cf_agents_runs (id, name, snapshot, created_at)
VALUES (${id}, ${name}, NULL, ${Date.now()})
`;
}
);
this.sql`
INSERT INTO cf_agents_runs (id, name, snapshot, created_at)
VALUES (${id}, ${name}, NULL, ${Date.now()})
`;
const startedAt = Date.now();
this._emit("fiber:run:started", {
fiberId: id,
Expand All @@ -5680,26 +5613,16 @@ export class Agent<

const writeSnapshot = (data: unknown) => {
const snapshot = JSON.stringify(data);
this._withAgentSpan(
"persist_fiber_snapshot",
"fiber",
{
"cloudflare.agents.fiber.id": id,
"cloudflare.agents.fiber.name": name
},
() => {
this.sql`
UPDATE cf_agents_runs SET snapshot = ${snapshot}
WHERE id = ${id}
`;
if (options?.managed) {
this.sql`
UPDATE cf_agents_fibers SET snapshot = ${snapshot}
WHERE fiber_id = ${id}
`;
}
}
);
this.sql`
UPDATE cf_agents_runs SET snapshot = ${snapshot}
WHERE id = ${id}
`;
if (options?.managed) {
this.sql`
UPDATE cf_agents_fibers SET snapshot = ${snapshot}
WHERE fiber_id = ${id}
`;
}
};

let root: RootFacetRpcSurface | undefined;
Expand Down Expand Up @@ -5746,17 +5669,7 @@ export class Agent<
}
} finally {
this._runFiberActiveFibers.delete(id);
this._withAgentSpan(
"finalize_fiber",
"fiber",
{
"cloudflare.agents.fiber.id": id,
"cloudflare.agents.fiber.name": name
},
() => {
this.sql`DELETE FROM cf_agents_runs WHERE id = ${id}`;
}
);
this.sql`DELETE FROM cf_agents_runs WHERE id = ${id}`;
dispose();
if (root && registeredFacetRun) {
try {
Expand Down Expand Up @@ -6486,12 +6399,6 @@ export class Agent<
}

private async _scheduleNextAlarm(): Promise<void> {
await this._withAgentSpan("schedule_agent_alarm", "alarm", {}, () =>
this._scheduleNextAlarmBody()
);
}

private async _scheduleNextAlarmBody(): Promise<void> {
// A pending destroy (#1625) owns the alarm: keep it armed immediately so
// teardown lands, and never let the "no work pending" branch below
// delete it out from under `_cf_scheduleDestroy`.
Expand Down
Loading
Loading